@foxden-app/foxclaw 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/.env.example +36 -0
  2. package/LICENSE +22 -0
  3. package/README.md +244 -0
  4. package/README_EN.md +244 -0
  5. package/dist/channels/bridge_messaging_router.d.ts +27 -0
  6. package/dist/channels/bridge_messaging_router.js +85 -0
  7. package/dist/channels/telegram/telegram_channel_adapter.d.ts +12 -0
  8. package/dist/channels/telegram/telegram_channel_adapter.js +21 -0
  9. package/dist/channels/telegram/telegram_messaging_port.d.ts +25 -0
  10. package/dist/channels/telegram/telegram_messaging_port.js +51 -0
  11. package/dist/channels/weixin/account_store.d.ts +15 -0
  12. package/dist/channels/weixin/account_store.js +54 -0
  13. package/dist/channels/weixin/ilink/aes_ecb.d.ts +3 -0
  14. package/dist/channels/weixin/ilink/aes_ecb.js +12 -0
  15. package/dist/channels/weixin/ilink/api.d.ts +44 -0
  16. package/dist/channels/weixin/ilink/api.js +187 -0
  17. package/dist/channels/weixin/ilink/cdn_upload.d.ts +11 -0
  18. package/dist/channels/weixin/ilink/cdn_upload.js +60 -0
  19. package/dist/channels/weixin/ilink/cdn_url.d.ts +7 -0
  20. package/dist/channels/weixin/ilink/cdn_url.js +7 -0
  21. package/dist/channels/weixin/ilink/constants.d.ts +7 -0
  22. package/dist/channels/weixin/ilink/constants.js +27 -0
  23. package/dist/channels/weixin/ilink/context.d.ts +13 -0
  24. package/dist/channels/weixin/ilink/context.js +13 -0
  25. package/dist/channels/weixin/ilink/login_qr.d.ts +34 -0
  26. package/dist/channels/weixin/ilink/login_qr.js +233 -0
  27. package/dist/channels/weixin/ilink/media_image.d.ts +11 -0
  28. package/dist/channels/weixin/ilink/media_image.js +44 -0
  29. package/dist/channels/weixin/ilink/mime.d.ts +3 -0
  30. package/dist/channels/weixin/ilink/mime.js +36 -0
  31. package/dist/channels/weixin/ilink/pic_decrypt.d.ts +2 -0
  32. package/dist/channels/weixin/ilink/pic_decrypt.js +56 -0
  33. package/dist/channels/weixin/ilink/random.d.ts +2 -0
  34. package/dist/channels/weixin/ilink/random.js +7 -0
  35. package/dist/channels/weixin/ilink/redact.d.ts +4 -0
  36. package/dist/channels/weixin/ilink/redact.js +34 -0
  37. package/dist/channels/weixin/ilink/runtime_attach.d.ts +3 -0
  38. package/dist/channels/weixin/ilink/runtime_attach.js +13 -0
  39. package/dist/channels/weixin/ilink/send.d.ts +21 -0
  40. package/dist/channels/weixin/ilink/send.js +108 -0
  41. package/dist/channels/weixin/ilink/session_guard.d.ts +6 -0
  42. package/dist/channels/weixin/ilink/session_guard.js +39 -0
  43. package/dist/channels/weixin/ilink/types.d.ts +155 -0
  44. package/dist/channels/weixin/ilink/types.js +10 -0
  45. package/dist/channels/weixin/ilink/upload.d.ts +15 -0
  46. package/dist/channels/weixin/ilink/upload.js +75 -0
  47. package/dist/channels/weixin/sync_buf_store.d.ts +3 -0
  48. package/dist/channels/weixin/sync_buf_store.js +19 -0
  49. package/dist/channels/weixin/weixin_channel_adapter.d.ts +18 -0
  50. package/dist/channels/weixin/weixin_channel_adapter.js +273 -0
  51. package/dist/channels/weixin/weixin_messaging_port.d.ts +29 -0
  52. package/dist/channels/weixin/weixin_messaging_port.js +113 -0
  53. package/dist/codex_app/client.d.ts +176 -0
  54. package/dist/codex_app/client.js +1230 -0
  55. package/dist/codex_app/deeplink.d.ts +7 -0
  56. package/dist/codex_app/deeplink.js +29 -0
  57. package/dist/codex_app/local_usage.d.ts +16 -0
  58. package/dist/codex_app/local_usage.js +123 -0
  59. package/dist/config.d.ts +44 -0
  60. package/dist/config.js +131 -0
  61. package/dist/controller/access.d.ts +11 -0
  62. package/dist/controller/access.js +33 -0
  63. package/dist/controller/activity.d.ts +62 -0
  64. package/dist/controller/activity.js +330 -0
  65. package/dist/controller/commands.d.ts +6 -0
  66. package/dist/controller/commands.js +17 -0
  67. package/dist/controller/controller.d.ts +326 -0
  68. package/dist/controller/controller.js +7503 -0
  69. package/dist/controller/observer.d.ts +16 -0
  70. package/dist/controller/observer.js +98 -0
  71. package/dist/controller/presentation.d.ts +80 -0
  72. package/dist/controller/presentation.js +568 -0
  73. package/dist/controller/service_tier.d.ts +9 -0
  74. package/dist/controller/service_tier.js +32 -0
  75. package/dist/controller/session_observer.d.ts +22 -0
  76. package/dist/controller/session_observer.js +259 -0
  77. package/dist/controller/status.d.ts +10 -0
  78. package/dist/controller/status.js +28 -0
  79. package/dist/core/bridge_scope.d.ts +18 -0
  80. package/dist/core/bridge_scope.js +46 -0
  81. package/dist/core/channel_port.d.ts +15 -0
  82. package/dist/core/channel_port.js +1 -0
  83. package/dist/i18n.d.ts +1108 -0
  84. package/dist/i18n.js +1154 -0
  85. package/dist/lock.d.ts +7 -0
  86. package/dist/lock.js +80 -0
  87. package/dist/logger.d.ts +12 -0
  88. package/dist/logger.js +57 -0
  89. package/dist/main.d.ts +2 -0
  90. package/dist/main.js +236 -0
  91. package/dist/runtime.d.ts +3 -0
  92. package/dist/runtime.js +14 -0
  93. package/dist/store/database.d.ts +79 -0
  94. package/dist/store/database.js +489 -0
  95. package/dist/store/migrate_bridge_scope.d.ts +6 -0
  96. package/dist/store/migrate_bridge_scope.js +59 -0
  97. package/dist/telegram/addressing.d.ts +33 -0
  98. package/dist/telegram/addressing.js +57 -0
  99. package/dist/telegram/api.d.ts +14 -0
  100. package/dist/telegram/api.js +89 -0
  101. package/dist/telegram/gateway.d.ts +76 -0
  102. package/dist/telegram/gateway.js +383 -0
  103. package/dist/telegram/media.d.ts +34 -0
  104. package/dist/telegram/media.js +180 -0
  105. package/dist/telegram/rendering.d.ts +10 -0
  106. package/dist/telegram/rendering.js +21 -0
  107. package/dist/telegram/scope.d.ts +6 -0
  108. package/dist/telegram/scope.js +24 -0
  109. package/dist/telegram/text.d.ts +7 -0
  110. package/dist/telegram/text.js +47 -0
  111. package/dist/types.d.ts +343 -0
  112. package/dist/types.js +1 -0
  113. package/docs/agent-assisted-install.md +84 -0
  114. package/docs/install-for-beginners.md +287 -0
  115. package/docs/troubleshooting.md +239 -0
  116. package/package.json +62 -0
  117. package/scripts/doctor.sh +3 -0
  118. package/scripts/launchd/install.sh +54 -0
  119. package/scripts/status.sh +3 -0
  120. package/scripts/systemd/install.sh +83 -0
  121. package/scripts/systemd/uninstall.sh +15 -0
  122. package/skills/foxclaw/SKILL.md +167 -0
  123. package/skills/foxclaw/agents/openai.yaml +4 -0
  124. package/skills/foxclaw/references/telegram-setup.md +93 -0
  125. package/skills/foxclaw/scripts/bootstrap_host.py +350 -0
  126. package/skills/foxclaw/scripts/bootstrap_remote.py +67 -0
package/dist/lock.d.ts ADDED
@@ -0,0 +1,7 @@
1
+ export interface ProcessLock {
2
+ release(): void;
3
+ }
4
+ export declare class LockHeldError extends Error {
5
+ constructor(lockPath: string, pid: number | null);
6
+ }
7
+ export declare function acquireProcessLock(lockPath: string): ProcessLock;
package/dist/lock.js ADDED
@@ -0,0 +1,80 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import process from 'node:process';
4
+ export class LockHeldError extends Error {
5
+ constructor(lockPath, pid) {
6
+ super(pid === null
7
+ ? `Lock already held: ${lockPath}`
8
+ : `Lock already held by pid ${pid}: ${lockPath}`);
9
+ }
10
+ }
11
+ export function acquireProcessLock(lockPath) {
12
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
13
+ return acquireProcessLockInternal(lockPath, true);
14
+ }
15
+ function acquireProcessLockInternal(lockPath, allowStaleRetry) {
16
+ try {
17
+ const fd = fs.openSync(lockPath, 'wx');
18
+ fs.writeFileSync(fd, `${process.pid}\n`, 'utf8');
19
+ let released = false;
20
+ return {
21
+ release() {
22
+ if (released) {
23
+ return;
24
+ }
25
+ released = true;
26
+ try {
27
+ fs.closeSync(fd);
28
+ }
29
+ catch {
30
+ void 0;
31
+ }
32
+ try {
33
+ fs.rmSync(lockPath, { force: true });
34
+ }
35
+ catch {
36
+ void 0;
37
+ }
38
+ },
39
+ };
40
+ }
41
+ catch (error) {
42
+ if (!isAlreadyExistsError(error)) {
43
+ throw error;
44
+ }
45
+ const pid = readLockPid(lockPath);
46
+ if (allowStaleRetry && pid !== null && !isProcessAlive(pid)) {
47
+ fs.rmSync(lockPath, { force: true });
48
+ return acquireProcessLockInternal(lockPath, false);
49
+ }
50
+ throw new LockHeldError(lockPath, pid);
51
+ }
52
+ }
53
+ function readLockPid(lockPath) {
54
+ try {
55
+ const value = fs.readFileSync(lockPath, 'utf8').trim();
56
+ if (!value) {
57
+ return null;
58
+ }
59
+ const pid = Number.parseInt(value, 10);
60
+ return Number.isFinite(pid) ? pid : null;
61
+ }
62
+ catch {
63
+ return null;
64
+ }
65
+ }
66
+ function isProcessAlive(pid) {
67
+ if (!Number.isFinite(pid) || pid <= 0) {
68
+ return false;
69
+ }
70
+ try {
71
+ process.kill(pid, 0);
72
+ return true;
73
+ }
74
+ catch (error) {
75
+ return error?.code === 'EPERM';
76
+ }
77
+ }
78
+ function isAlreadyExistsError(error) {
79
+ return typeof error === 'object' && error !== null && 'code' in error && error.code === 'EEXIST';
80
+ }
@@ -0,0 +1,12 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export declare class Logger {
3
+ private level;
4
+ private filePath;
5
+ constructor(level: LogLevel, filePath: string);
6
+ debug(message: string, meta?: unknown): void;
7
+ info(message: string, meta?: unknown): void;
8
+ warn(message: string, meta?: unknown): void;
9
+ error(message: string, meta?: unknown): void;
10
+ private write;
11
+ private rotateIfNeeded;
12
+ }
package/dist/logger.js ADDED
@@ -0,0 +1,57 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const RANK = { debug: 10, info: 20, warn: 30, error: 40 };
4
+ const MAX_SIZE = 1_000_000;
5
+ export class Logger {
6
+ level;
7
+ filePath;
8
+ constructor(level, filePath) {
9
+ this.level = level;
10
+ this.filePath = filePath;
11
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
12
+ }
13
+ debug(message, meta) { this.write('debug', message, meta); }
14
+ info(message, meta) { this.write('info', message, meta); }
15
+ warn(message, meta) { this.write('warn', message, meta); }
16
+ error(message, meta) { this.write('error', message, meta); }
17
+ write(level, message, meta) {
18
+ if (RANK[level] < RANK[this.level])
19
+ return;
20
+ const record = {
21
+ time: new Date().toISOString(),
22
+ level,
23
+ message,
24
+ ...(meta === undefined ? {} : { meta })
25
+ };
26
+ const line = JSON.stringify(record);
27
+ if (level === 'error') {
28
+ console.error(line);
29
+ }
30
+ else if (level === 'warn') {
31
+ console.warn(line);
32
+ }
33
+ else {
34
+ console.log(line);
35
+ }
36
+ this.rotateIfNeeded();
37
+ fs.appendFileSync(this.filePath, line + '\n', 'utf8');
38
+ }
39
+ rotateIfNeeded() {
40
+ try {
41
+ const stat = fs.statSync(this.filePath);
42
+ if (stat.size < MAX_SIZE)
43
+ return;
44
+ const rotated = `${this.filePath}.1`;
45
+ try {
46
+ fs.rmSync(rotated, { force: true });
47
+ }
48
+ catch {
49
+ return;
50
+ }
51
+ fs.renameSync(this.filePath, rotated);
52
+ }
53
+ catch {
54
+ // ignore missing file
55
+ }
56
+ }
57
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import process from 'node:process';
5
+ import { spawnSync } from 'node:child_process';
6
+ import dotenv from 'dotenv';
7
+ import { APP_HOME, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, loadConfig } from './config.js';
8
+ import { BridgeMessagingRouter } from './channels/bridge_messaging_router.js';
9
+ import { TelegramMessagingPort } from './channels/telegram/telegram_messaging_port.js';
10
+ import { WeixinChannelAdapter } from './channels/weixin/weixin_channel_adapter.js';
11
+ import { WeixinMessagingPort } from './channels/weixin/weixin_messaging_port.js';
12
+ import { attachIlinkRuntimeFromBridgeLogger } from './channels/weixin/ilink/runtime_attach.js';
13
+ import { startWeixinLoginWithQr, waitForWeixinLogin } from './channels/weixin/ilink/login_qr.js';
14
+ import { accountFilePath, loadWeixinAccount, saveWeixinAccount } from './channels/weixin/account_store.js';
15
+ import { Logger } from './logger.js';
16
+ import { BridgeStore } from './store/database.js';
17
+ import { TelegramGateway } from './telegram/gateway.js';
18
+ import { CodexAppClient } from './codex_app/client.js';
19
+ import { BridgeSessionCore } from './controller/controller.js';
20
+ import { TelegramChannelAdapter } from './channels/telegram/telegram_channel_adapter.js';
21
+ import { acquireProcessLock, LockHeldError } from './lock.js';
22
+ import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
23
+ const command = process.argv[2] || 'serve';
24
+ dotenv.config();
25
+ async function main() {
26
+ if (command === 'status') {
27
+ const status = readRuntimeStatus(process.env.STATUS_PATH || DEFAULT_STATUS_PATH);
28
+ if (!status) {
29
+ console.log('No runtime status found.');
30
+ process.exit(1);
31
+ }
32
+ console.log(JSON.stringify(status, null, 2));
33
+ return;
34
+ }
35
+ if (command === 'doctor') {
36
+ const configuredCodexBin = process.env.CODEX_CLI_BIN;
37
+ const checks = [
38
+ ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
39
+ ['codex cli available', hasConfiguredCodexBin(configuredCodexBin) || hasCommand('codex')],
40
+ ['telegram bot token configured', Boolean(process.env.TG_BOT_TOKEN)],
41
+ ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
42
+ ];
43
+ if (process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1') {
44
+ const accountsDir = process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts');
45
+ let hasAccounts = false;
46
+ try {
47
+ if (fs.existsSync(accountsDir)) {
48
+ hasAccounts = fs.readdirSync(accountsDir).some((n) => n.endsWith('.json'));
49
+ }
50
+ }
51
+ catch {
52
+ hasAccounts = false;
53
+ }
54
+ checks.push(['WX_ENABLED: Weixin account JSON present', hasAccounts]);
55
+ checks.push(['WX_ALLOWED_ILINK_USER_IDS set (recommended)', Boolean(process.env.WX_ALLOWED_ILINK_USER_IDS?.trim())]);
56
+ }
57
+ let failed = false;
58
+ for (const [name, ok] of checks) {
59
+ console.log(`${ok ? '[OK]' : '[FAIL]'} ${name}`);
60
+ if (!ok)
61
+ failed = true;
62
+ }
63
+ try {
64
+ const cwd = process.env.DEFAULT_CWD || process.cwd();
65
+ fs.accessSync(cwd);
66
+ console.log(`[OK] default cwd exists: ${cwd}`);
67
+ }
68
+ catch {
69
+ const cwd = process.env.DEFAULT_CWD || process.cwd();
70
+ console.log(`[FAIL] default cwd missing: ${cwd}`);
71
+ failed = true;
72
+ }
73
+ process.exit(failed ? 1 : 0);
74
+ }
75
+ if (command === 'weixin-login') {
76
+ await runWeixinLoginCli();
77
+ return;
78
+ }
79
+ const config = loadConfig();
80
+ const logger = new Logger(config.logLevel, config.logPath);
81
+ attachIlinkRuntimeFromBridgeLogger(logger, config.wxIlinkRouteTag);
82
+ const processLock = acquireProcessLock(config.lockPath);
83
+ let store = null;
84
+ let weixinAdapter = null;
85
+ try {
86
+ store = new BridgeStore(config.storePath);
87
+ const bot = new TelegramGateway(config.tgBotToken, config.tgAllowedUserId, config.tgAllowedChatId, config.telegramPollIntervalMs, store, logger);
88
+ const app = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger);
89
+ const telegramMessaging = new TelegramMessagingPort(bot);
90
+ const weixinMessaging = config.wxEnabled
91
+ ? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
92
+ : null;
93
+ const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
94
+ const core = new BridgeSessionCore(config, store, logger, bot, app, outbound);
95
+ const telegram = new TelegramChannelAdapter(core);
96
+ if (config.wxEnabled) {
97
+ weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
98
+ }
99
+ process.on('unhandledRejection', (error) => {
100
+ logger.error('process.unhandled_rejection', { error: serializeError(error) });
101
+ });
102
+ process.on('uncaughtException', (error) => {
103
+ logger.error('process.uncaught_exception', { error: serializeError(error) });
104
+ });
105
+ await telegram.start();
106
+ if (weixinAdapter) {
107
+ await weixinAdapter.start();
108
+ }
109
+ logger.info('bridge.started', core.getRuntimeStatus());
110
+ const shutdown = async (signal) => {
111
+ logger.info('bridge.shutting_down', { signal });
112
+ await weixinAdapter?.stop();
113
+ await telegram.stop();
114
+ writeRuntimeStatus(config.statusPath, {
115
+ running: false,
116
+ connected: false,
117
+ userAgent: app.getUserAgent(),
118
+ codexAppServer: app.getServerStatus(),
119
+ botUsername: bot.username,
120
+ currentBindings: 0,
121
+ pendingApprovals: 0,
122
+ pendingUserInputs: 0,
123
+ activeTurns: 0,
124
+ lastError: null,
125
+ updatedAt: new Date().toISOString(),
126
+ channels: { telegram: false, weixin: false },
127
+ });
128
+ store?.close();
129
+ processLock.release();
130
+ process.exit(0);
131
+ };
132
+ process.on('SIGINT', () => void shutdown('SIGINT'));
133
+ process.on('SIGTERM', () => void shutdown('SIGTERM'));
134
+ }
135
+ catch (error) {
136
+ await weixinAdapter?.stop().catch(() => { });
137
+ store?.close();
138
+ processLock.release();
139
+ throw error;
140
+ }
141
+ }
142
+ async function runWeixinLoginCli() {
143
+ const logPath = process.env.LOG_PATH || DEFAULT_LOG_PATH;
144
+ const level = process.env.LOG_LEVEL === 'debug' || process.env.LOG_LEVEL === 'warn' || process.env.LOG_LEVEL === 'error'
145
+ ? process.env.LOG_LEVEL
146
+ : 'info';
147
+ const logger = new Logger(level, logPath);
148
+ attachIlinkRuntimeFromBridgeLogger(logger, process.env.WX_ILINK_ROUTE_TAG ?? null);
149
+ const accountsDir = process.env.WEIXIN_ACCOUNTS_DIR || path.join(APP_HOME, 'weixin', 'accounts');
150
+ fs.mkdirSync(accountsDir, { recursive: true });
151
+ const accountHint = process.argv.slice(3).find((a) => !a.startsWith('-'))?.trim();
152
+ const start = await startWeixinLoginWithQr({
153
+ apiBaseUrl: 'https://ilinkai.weixin.qq.com',
154
+ force: process.env.WX_LOGIN_FORCE === '1' || process.env.WX_LOGIN_FORCE === 'true',
155
+ ...(accountHint ? { accountId: accountHint } : {}),
156
+ });
157
+ if (!start.qrcodeUrl) {
158
+ console.error(start.message);
159
+ process.exit(1);
160
+ }
161
+ console.log(start.message);
162
+ try {
163
+ const qrterm = await import('qrcode-terminal');
164
+ qrterm.default.generate(start.qrcodeUrl, { small: true });
165
+ }
166
+ catch {
167
+ console.log('Open this URL in a browser to scan:', start.qrcodeUrl);
168
+ }
169
+ const wait = await waitForWeixinLogin({
170
+ sessionKey: start.sessionKey,
171
+ apiBaseUrl: 'https://ilinkai.weixin.qq.com',
172
+ verbose: process.env.WX_LOGIN_VERBOSE === '1',
173
+ notify: (s) => process.stdout.write(s),
174
+ onQrRefreshed: async (url) => {
175
+ try {
176
+ const qrterm = await import('qrcode-terminal');
177
+ qrterm.default.generate(url, { small: true });
178
+ process.stdout.write(`If the QR did not render, open:\n${url}\n`);
179
+ }
180
+ catch {
181
+ process.stdout.write(`Open this URL to scan:\n${url}\n`);
182
+ }
183
+ },
184
+ });
185
+ if (!wait.connected || !wait.accountId || !wait.botToken) {
186
+ console.error(wait.message);
187
+ process.exit(1);
188
+ }
189
+ let baseUrl = wait.baseUrl?.trim() || 'https://ilink.weixin.qq.com';
190
+ if (!/^https?:\/\//i.test(baseUrl)) {
191
+ baseUrl = `https://${baseUrl}`;
192
+ }
193
+ saveWeixinAccount(accountsDir, {
194
+ accountId: wait.accountId,
195
+ botToken: wait.botToken,
196
+ baseUrl,
197
+ savedAt: Date.now(),
198
+ ...(wait.userId !== undefined ? { linkedIlinkUserId: wait.userId } : {}),
199
+ });
200
+ console.log(`${wait.message} Account saved to ${accountFilePath(accountsDir, wait.accountId)}`);
201
+ }
202
+ void main().catch((error) => {
203
+ if (error instanceof LockHeldError) {
204
+ console.error(error.message);
205
+ process.exit(1);
206
+ }
207
+ console.error(error);
208
+ process.exit(1);
209
+ });
210
+ function hasCommand(commandName) {
211
+ try {
212
+ const which = process.platform === 'win32' ? 'where' : 'which';
213
+ const result = spawnSync(which, [commandName], { stdio: 'ignore' });
214
+ return result.status === 0;
215
+ }
216
+ catch {
217
+ return false;
218
+ }
219
+ }
220
+ function hasConfiguredCodexBin(binPath) {
221
+ if (!binPath || !binPath.trim())
222
+ return false;
223
+ try {
224
+ fs.accessSync(binPath, fs.constants.X_OK);
225
+ return true;
226
+ }
227
+ catch {
228
+ return false;
229
+ }
230
+ }
231
+ function serializeError(error) {
232
+ if (error instanceof Error) {
233
+ return { message: error.message, stack: error.stack };
234
+ }
235
+ return { error: String(error) };
236
+ }
@@ -0,0 +1,3 @@
1
+ import type { RuntimeStatus } from './types.js';
2
+ export declare function writeRuntimeStatus(path: string, status: RuntimeStatus): void;
3
+ export declare function readRuntimeStatus(path: string): RuntimeStatus | null;
@@ -0,0 +1,14 @@
1
+ import fs from 'node:fs';
2
+ export function writeRuntimeStatus(path, status) {
3
+ const tmp = `${path}.tmp`;
4
+ fs.writeFileSync(tmp, JSON.stringify(status, null, 2), 'utf8');
5
+ fs.renameSync(tmp, path);
6
+ }
7
+ export function readRuntimeStatus(path) {
8
+ try {
9
+ return JSON.parse(fs.readFileSync(path, 'utf8'));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
@@ -0,0 +1,79 @@
1
+ import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, CachedThread, ChatSessionSettings, CollaborationModeValue, PendingApprovalRecord, ReasoningEffortValue, ThreadBinding } from '../types.js';
2
+ export interface ActiveTurnPreviewRecord {
3
+ turnId: string;
4
+ scopeId: string;
5
+ threadId: string;
6
+ messageId: number;
7
+ createdAt: number;
8
+ updatedAt: number;
9
+ }
10
+ export interface PendingUserInputStoredRecord {
11
+ localId: string;
12
+ serverRequestId: string;
13
+ chatId: string;
14
+ threadId: string;
15
+ turnId: string | null;
16
+ itemId: string;
17
+ messageId: number | null;
18
+ questionsJson: string;
19
+ answersJson: string;
20
+ currentQuestionIndex: number;
21
+ awaitingFreeText: boolean;
22
+ status: string;
23
+ createdAt: number;
24
+ submittedAt: number | null;
25
+ resolvedAt: number | null;
26
+ }
27
+ export declare class BridgeStore {
28
+ private db;
29
+ constructor(dbPath: string);
30
+ getTelegramOffset(botKey: string): number;
31
+ setTelegramOffset(botKey: string, updateId: number): void;
32
+ getBinding(chatId: string): ThreadBinding | null;
33
+ setBinding(chatId: string, threadId: string, cwd: string | null): void;
34
+ clearBinding(chatId: string): void;
35
+ getChatSettings(chatId: string): ChatSessionSettings | null;
36
+ setChatSettings(chatId: string, model: string | null, reasoningEffort: ReasoningEffortValue | null, locale?: AppLocale | null): void;
37
+ setChatLocale(chatId: string, locale: AppLocale): void;
38
+ setChatAccessPreset(chatId: string, accessPreset: AccessPresetValue | null): void;
39
+ setChatCollaborationMode(chatId: string, collaborationMode: CollaborationModeValue | null): void;
40
+ setChatServiceTier(chatId: string, serviceTier: string | null): void;
41
+ setChatActiveTurnMessageMode(chatId: string, activeTurnMessageMode: ActiveTurnMessageMode | null): void;
42
+ findChatIdByThreadId(threadId: string): string | null;
43
+ findAllChatIdsByThreadId(threadId: string): string[];
44
+ countBindings(): number;
45
+ cacheThreadList(chatId: string, threads: Array<Omit<CachedThread, 'index' | 'archived'> & {
46
+ listIndex?: number;
47
+ archived?: boolean;
48
+ }>): void;
49
+ getCachedThread(chatId: string, index: number): CachedThread | null;
50
+ listCachedThreads(chatId: string): CachedThread[];
51
+ savePendingApproval(record: PendingApprovalRecord): void;
52
+ updatePendingApprovalMessage(localId: string, messageId: number): void;
53
+ getPendingApproval(localId: string): PendingApprovalRecord | null;
54
+ getPendingApprovalByServerRequestId(serverRequestId: string): PendingApprovalRecord | null;
55
+ markApprovalResolved(localId: string): void;
56
+ countPendingApprovals(): number;
57
+ saveActiveTurnPreview(record: Pick<ActiveTurnPreviewRecord, 'turnId' | 'scopeId' | 'threadId' | 'messageId'>): void;
58
+ listActiveTurnPreviews(): ActiveTurnPreviewRecord[];
59
+ removeActiveTurnPreview(turnId: string): void;
60
+ removeActiveTurnPreviewByMessage(scopeId: string, messageId: number): void;
61
+ savePendingUserInput(record: PendingUserInputStoredRecord): void;
62
+ updatePendingUserInputMessage(localId: string, messageId: number): void;
63
+ updatePendingUserInputAnswers(localId: string, answersJson: string, currentQuestionIndex: number, awaitingFreeText?: boolean): void;
64
+ markPendingUserInputSubmitted(localId: string): void;
65
+ markPendingUserInputResolved(localId: string): void;
66
+ markPendingUserInputInterrupted(localId: string): void;
67
+ listPendingUserInputs(): PendingUserInputStoredRecord[];
68
+ countPendingUserInputs(): number;
69
+ insertAudit(direction: 'inbound' | 'outbound', chatId: string, eventType: string, summary: string): void;
70
+ close(): void;
71
+ private mapApproval;
72
+ private mapPendingUserInput;
73
+ private writeChatSettings;
74
+ getWeixinContextToken(scopeId: string): string | null;
75
+ setWeixinContextToken(scopeId: string, contextToken: string): void;
76
+ listDisabledCodexAuthCandidateNames(): Set<string>;
77
+ setCodexAuthCandidateDisabled(name: string, disabled: boolean): void;
78
+ private ensureColumn;
79
+ }