ai-agent-router 0.1.21 → 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 (133) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/build-manifest.json +2 -2
  3. package/.next/fallback-build-manifest.json +2 -2
  4. package/.next/server/app/_global-error.html +2 -2
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/server/app/_not-found.html +1 -1
  12. package/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/server/app/api/config/route.js.nft.json +1 -1
  20. package/.next/server/app/api/gateway/[...path]/route.js.nft.json +1 -1
  21. package/.next/server/app/api/gateway/models/route.js.nft.json +1 -1
  22. package/.next/server/app/api/gateway/route.js.nft.json +1 -1
  23. package/.next/server/app/api/ide/claude/apply/route.js.nft.json +1 -1
  24. package/.next/server/app/api/ide/claude/available-models/route.js.nft.json +1 -1
  25. package/.next/server/app/api/ide/claude/save/route.js.nft.json +1 -1
  26. package/.next/server/app/api/ide/claude/status/route.js.nft.json +1 -1
  27. package/.next/server/app/api/ide/claude/test/route.js.nft.json +1 -1
  28. package/.next/server/app/api/logs/route.js.nft.json +1 -1
  29. package/.next/server/app/api/models/route.js.nft.json +1 -1
  30. package/.next/server/app/api/providers/route.js.nft.json +1 -1
  31. package/.next/server/app/api/providers/test/route.js.nft.json +1 -1
  32. package/.next/server/app/api/service/force-stop/route.js.nft.json +1 -1
  33. package/.next/server/app/api/service/start/route.js.nft.json +1 -1
  34. package/.next/server/app/api/service/status/route.js.nft.json +1 -1
  35. package/.next/server/app/api/service/stop/route.js.nft.json +1 -1
  36. package/.next/server/app/ide.html +1 -1
  37. package/.next/server/app/ide.rsc +1 -1
  38. package/.next/server/app/ide.segments/_full.segment.rsc +1 -1
  39. package/.next/server/app/ide.segments/_head.segment.rsc +1 -1
  40. package/.next/server/app/ide.segments/_index.segment.rsc +1 -1
  41. package/.next/server/app/ide.segments/_tree.segment.rsc +1 -1
  42. package/.next/server/app/ide.segments/ide/__PAGE__.segment.rsc +1 -1
  43. package/.next/server/app/ide.segments/ide.segment.rsc +1 -1
  44. package/.next/server/app/index.html +1 -1
  45. package/.next/server/app/index.rsc +1 -1
  46. package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  47. package/.next/server/app/index.segments/_full.segment.rsc +1 -1
  48. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  49. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  50. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/.next/server/app/logs/page_client-reference-manifest.js +1 -1
  52. package/.next/server/app/logs.html +1 -1
  53. package/.next/server/app/logs.rsc +2 -2
  54. package/.next/server/app/logs.segments/_full.segment.rsc +2 -2
  55. package/.next/server/app/logs.segments/_head.segment.rsc +1 -1
  56. package/.next/server/app/logs.segments/_index.segment.rsc +1 -1
  57. package/.next/server/app/logs.segments/_tree.segment.rsc +1 -1
  58. package/.next/server/app/logs.segments/logs/__PAGE__.segment.rsc +2 -2
  59. package/.next/server/app/logs.segments/logs.segment.rsc +1 -1
  60. package/.next/server/app/models.html +1 -1
  61. package/.next/server/app/models.rsc +1 -1
  62. package/.next/server/app/models.segments/_full.segment.rsc +1 -1
  63. package/.next/server/app/models.segments/_head.segment.rsc +1 -1
  64. package/.next/server/app/models.segments/_index.segment.rsc +1 -1
  65. package/.next/server/app/models.segments/_tree.segment.rsc +1 -1
  66. package/.next/server/app/models.segments/models/__PAGE__.segment.rsc +1 -1
  67. package/.next/server/app/models.segments/models.segment.rsc +1 -1
  68. package/.next/server/app/providers.html +1 -1
  69. package/.next/server/app/providers.rsc +1 -1
  70. package/.next/server/app/providers.segments/_full.segment.rsc +1 -1
  71. package/.next/server/app/providers.segments/_head.segment.rsc +1 -1
  72. package/.next/server/app/providers.segments/_index.segment.rsc +1 -1
  73. package/.next/server/app/providers.segments/_tree.segment.rsc +1 -1
  74. package/.next/server/app/providers.segments/providers/__PAGE__.segment.rsc +1 -1
  75. package/.next/server/app/providers.segments/providers.segment.rsc +1 -1
  76. package/.next/server/chunks/[root-of-the-server]__1480f018._.js +1 -1
  77. package/.next/server/chunks/[root-of-the-server]__1480f018._.js.map +1 -1
  78. package/.next/server/chunks/[root-of-the-server]__1909f3aa._.js +1 -1
  79. package/.next/server/chunks/[root-of-the-server]__1909f3aa._.js.map +1 -1
  80. package/.next/server/chunks/[root-of-the-server]__1d4b7fc5._.js +1 -1
  81. package/.next/server/chunks/[root-of-the-server]__1d4b7fc5._.js.map +1 -1
  82. package/.next/server/chunks/[root-of-the-server]__372ef2bf._.js +1 -1
  83. package/.next/server/chunks/[root-of-the-server]__372ef2bf._.js.map +1 -1
  84. package/.next/server/chunks/[root-of-the-server]__3aaf963c._.js +1 -1
  85. package/.next/server/chunks/[root-of-the-server]__3aaf963c._.js.map +1 -1
  86. package/.next/server/chunks/[root-of-the-server]__6ce199d2._.js +1 -1
  87. package/.next/server/chunks/[root-of-the-server]__6ce199d2._.js.map +1 -1
  88. package/.next/server/chunks/[root-of-the-server]__772134c6._.js +1 -1
  89. package/.next/server/chunks/[root-of-the-server]__772134c6._.js.map +1 -1
  90. package/.next/server/chunks/[root-of-the-server]__7b77f523._.js +1 -1
  91. package/.next/server/chunks/[root-of-the-server]__7b77f523._.js.map +1 -1
  92. package/.next/server/chunks/[root-of-the-server]__c1b4b601._.js +18 -18
  93. package/.next/server/chunks/[root-of-the-server]__c1b4b601._.js.map +1 -1
  94. package/.next/server/chunks/[root-of-the-server]__ccfc7f1d._.js +1 -1
  95. package/.next/server/chunks/[root-of-the-server]__ccfc7f1d._.js.map +1 -1
  96. package/.next/server/chunks/ssr/src_app_logs_page_tsx_7b7b7b83._.js +1 -1
  97. package/.next/server/chunks/ssr/src_app_logs_page_tsx_7b7b7b83._.js.map +1 -1
  98. package/.next/server/pages/404.html +1 -1
  99. package/.next/server/pages/500.html +2 -2
  100. package/.next/static/chunks/{81c904164fe81379.js → b6b258e8582e47c4.js} +1 -1
  101. package/README.md +100 -111
  102. package/dist/src/app/api/gateway/[...path]/route.js +1 -1
  103. package/dist/src/app/api/gateway/route.js +1 -1
  104. package/dist/src/app/api/logs/route.js +2 -2
  105. package/dist/src/app/api/models/route.js +5 -5
  106. package/dist/src/app/api/providers/route.js +4 -4
  107. package/dist/src/app/api/providers/test/route.js +1 -1
  108. package/dist/src/app/api/service/start/route.js +1 -1
  109. package/dist/src/app/api/service/status/route.js +1 -1
  110. package/dist/src/app/api/service/stop/route.js +1 -1
  111. package/dist/src/app/logs/page.js +13 -1
  112. package/dist/src/cli/index.js +218 -20
  113. package/dist/src/db/database.js +35 -1
  114. package/dist/src/db/queries.js +6 -6
  115. package/dist/src/server/logger.js +22 -4
  116. package/package.json +2 -1
  117. package/src/app/api/gateway/[...path]/route.ts +1 -1
  118. package/src/app/api/gateway/route.ts +1 -1
  119. package/src/app/api/logs/route.ts +2 -2
  120. package/src/app/api/models/route.ts +5 -5
  121. package/src/app/api/providers/route.ts +4 -4
  122. package/src/app/api/providers/test/route.ts +1 -1
  123. package/src/app/api/service/start/route.ts +1 -1
  124. package/src/app/api/service/status/route.ts +1 -1
  125. package/src/app/api/service/stop/route.ts +1 -1
  126. package/src/app/logs/page.tsx +15 -5
  127. package/src/cli/index.ts +228 -25
  128. package/src/db/database.ts +34 -4
  129. package/src/db/queries.ts +6 -6
  130. package/src/server/logger.ts +19 -4
  131. /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_buildManifest.js +0 -0
  132. /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_clientMiddlewareManifest.json +0 -0
  133. /package/.next/static/{PkfqdzwOZgX-UhSNUuhdp → ryTeHAYUvjT1bYolc-x9Z}/_ssgManifest.js +0 -0
package/src/cli/index.ts CHANGED
@@ -1,16 +1,88 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
- import { spawn } from 'child_process';
4
+ import { spawn, ChildProcess } from 'child_process';
5
5
  import path from 'path';
6
+ import fs from 'fs';
7
+ import os from 'os';
8
+ import http from 'http';
6
9
  import { getDatabase } from '../db/database';
7
- import { getConfig, setConfig } from '../db/queries';
10
+ import { getConfig, setConfig, setServiceStatus, updateServiceStatus } from '../db/queries';
8
11
 
9
12
  const program = new Command();
10
13
 
11
14
  const packageJsonPath = require.resolve('../../../package.json');
12
15
  const packageJson = require(packageJsonPath);
13
16
 
17
+ const AAR_DIR = path.join(os.homedir(), '.aar');
18
+ const UI_PID_FILE = path.join(AAR_DIR, 'ui.pid');
19
+ const GATEWAY_PID_FILE = path.join(AAR_DIR, 'gateway.pid');
20
+ const GATEWAY_PORT_FILE = path.join(AAR_DIR, 'gateway.port');
21
+
22
+ function ensureAarDir(): void {
23
+ if (!fs.existsSync(AAR_DIR)) {
24
+ fs.mkdirSync(AAR_DIR, { recursive: true });
25
+ }
26
+ }
27
+
28
+ function getPackageRoot(): string {
29
+ return path.dirname(path.dirname(path.dirname(__dirname)));
30
+ }
31
+
32
+ /** Wait for HTTP server to respond (e.g. UI or gateway ready) */
33
+ function waitForReady(baseUrl: string, maxWaitMs: number = 30000): Promise<boolean> {
34
+ const url = new URL(baseUrl);
35
+ return new Promise((resolve) => {
36
+ const start = Date.now();
37
+ const tryOnce = () => {
38
+ const req = http.request(
39
+ {
40
+ hostname: url.hostname,
41
+ port: url.port || '80',
42
+ path: url.pathname || '/',
43
+ method: 'GET',
44
+ timeout: 2000,
45
+ },
46
+ (res) => {
47
+ resolve(res.statusCode !== undefined && res.statusCode < 500);
48
+ }
49
+ );
50
+ req.on('error', () => {
51
+ if (Date.now() - start >= maxWaitMs) {
52
+ resolve(false);
53
+ return;
54
+ }
55
+ setTimeout(tryOnce, 800);
56
+ });
57
+ req.on('timeout', () => {
58
+ req.destroy();
59
+ if (Date.now() - start >= maxWaitMs) resolve(false);
60
+ else setTimeout(tryOnce, 800);
61
+ });
62
+ req.end();
63
+ };
64
+ tryOnce();
65
+ });
66
+ }
67
+
68
+ function printStatus(uiPort: number, gatewayPort: number, gatewayRunning: boolean, background: boolean): void {
69
+ const uiUrl = `http://localhost:${uiPort}`;
70
+ const gatewayUrl = `http://localhost:${gatewayPort}`;
71
+ console.log('');
72
+ console.log('-------------------------------------------');
73
+ console.log(' AI Agent Router');
74
+ console.log('-------------------------------------------');
75
+ console.log(` 前台 UI: ${uiUrl}`);
76
+ console.log(` 网关地址: ${gatewayUrl}`);
77
+ console.log(` 网关状态: ${gatewayRunning ? '运行中' : '未启动'}`);
78
+ if (background) {
79
+ console.log(' 运行模式: 后台运行(关闭终端不影响)');
80
+ console.log(' 停止服务: aar stop');
81
+ }
82
+ console.log('-------------------------------------------');
83
+ console.log('');
84
+ }
85
+
14
86
  program
15
87
  .name('aar')
16
88
  .description('AI Agent Router - Web UI for managing the API gateway')
@@ -18,47 +90,178 @@ program
18
90
 
19
91
  program
20
92
  .command('start')
21
- .description('Start the Web UI management interface')
93
+ .description('Start the Web UI and gateway (default: both; use --no-gateway for UI only)')
22
94
  .option('-p, --port <port>', 'Port for Web UI', '9527')
95
+ .option('-g, --gateway-port <port>', 'Port for gateway (default: from config or 1357)')
96
+ .option('--no-gateway', 'Only start Web UI, do not start gateway')
97
+ .option('--no-background', 'Run in foreground (attach to terminal)')
23
98
  .action(async (options) => {
24
- const port = parseInt(options.port || '9527');
25
-
26
- console.log(`Starting AI Agent Router Web UI`);
27
- console.log(` Port: ${port}`);
28
- console.log(` Access the UI at: http://localhost:${port}`);
29
- console.log('');
99
+ const uiPort = parseInt(options.port || '9527', 10);
100
+ const startGateway = options.gateway !== false;
101
+ const background = options.background !== false;
102
+ const packageRoot = getPackageRoot();
30
103
 
31
- // Get the package root directory (where package.json is located)
32
- const packageRoot = path.dirname(path.dirname(path.dirname(__dirname)));
104
+ let gatewayPort = 1357;
105
+ if (startGateway) {
106
+ try {
107
+ await getDatabase();
108
+ const portConfig = await getConfig('port');
109
+ gatewayPort = options.gatewayPort
110
+ ? parseInt(options.gatewayPort, 10)
111
+ : parseInt(portConfig?.value || '1357', 10);
112
+ } catch (e) {
113
+ gatewayPort = options.gatewayPort ? parseInt(options.gatewayPort, 10) : 1357;
114
+ }
115
+ }
33
116
 
34
- // Start Web UI using Next.js
35
- // Always use production mode for globally installed package
36
117
  const serverPath = path.join(packageRoot, 'node_modules', 'next', 'dist', 'bin', 'next');
37
- const uiProcess = spawn(process.execPath, [serverPath, 'start', '-p', port.toString()], {
118
+ const gatewayScriptPath = path.join(packageRoot, 'dist', 'src', 'cli', 'gateway-server.js');
119
+
120
+ if (background) {
121
+ ensureAarDir();
122
+ const envBase = { ...process.env, NODE_ENV: 'production' as const };
123
+
124
+ const uiProc: ChildProcess = spawn(process.execPath, [serverPath, 'start', '-p', uiPort.toString()], {
125
+ cwd: packageRoot,
126
+ env: { ...envBase, PORT: uiPort.toString() },
127
+ detached: true,
128
+ stdio: 'ignore',
129
+ });
130
+ uiProc.unref();
131
+ if (uiProc.pid) {
132
+ fs.writeFileSync(UI_PID_FILE, String(uiProc.pid));
133
+ }
134
+
135
+ let gatewayRunning = false;
136
+ if (startGateway && fs.existsSync(gatewayScriptPath)) {
137
+ const gwProc: ChildProcess = spawn(process.execPath, [gatewayScriptPath, '--port', String(gatewayPort)], {
138
+ cwd: packageRoot,
139
+ env: envBase,
140
+ detached: true,
141
+ stdio: 'ignore',
142
+ });
143
+ gwProc.unref();
144
+ if (gwProc.pid) {
145
+ fs.writeFileSync(GATEWAY_PID_FILE, String(gwProc.pid));
146
+ fs.writeFileSync(GATEWAY_PORT_FILE, String(gatewayPort));
147
+ gatewayRunning = true;
148
+ }
149
+ try {
150
+ await getDatabase();
151
+ await setServiceStatus({
152
+ status: 'running',
153
+ port: gatewayPort,
154
+ pid: gwProc.pid ?? null,
155
+ started_at: new Date().toISOString(),
156
+ });
157
+ } catch {
158
+ // ignore db errors
159
+ }
160
+ }
161
+
162
+ printStatus(uiPort, gatewayPort, gatewayRunning, true);
163
+ process.exit(0);
164
+ }
165
+
166
+ // Foreground: start UI first
167
+ console.log('Starting AI Agent Router...');
168
+ const uiProcess = spawn(process.execPath, [serverPath, 'start', '-p', uiPort.toString()], {
38
169
  cwd: packageRoot,
39
170
  stdio: ['ignore', 'inherit', 'inherit'],
40
- env: { ...process.env, PORT: port.toString(), NODE_ENV: 'production' },
41
- });
42
-
43
- // Handle UI process exit
44
- uiProcess.on('exit', (code) => {
45
- console.log(`Web UI process exited with code ${code}`);
46
- process.exit(code || 0);
171
+ env: { ...process.env, PORT: uiPort.toString(), NODE_ENV: 'production' },
47
172
  });
48
173
 
49
- uiProcess.on('error', (error) => {
174
+ uiProcess.on('error', (error: Error) => {
50
175
  console.error(`Failed to start Web UI: ${error.message}`);
51
176
  process.exit(1);
52
177
  });
53
178
 
54
- // Keep the process alive
55
- process.on('SIGINT', () => {
56
- console.log('\nShutting down...');
179
+ const gatewayProcess: ChildProcess | null = startGateway && fs.existsSync(gatewayScriptPath)
180
+ ? spawn(process.execPath, [gatewayScriptPath, '--port', String(gatewayPort)], {
181
+ cwd: packageRoot,
182
+ stdio: ['ignore', 'pipe', 'pipe'],
183
+ env: { ...process.env, NODE_ENV: 'production' },
184
+ })
185
+ : null;
186
+
187
+ if (gatewayProcess?.stdout) {
188
+ gatewayProcess.stdout.on('data', (d) => process.stdout.write(d));
189
+ }
190
+ if (gatewayProcess?.stderr) {
191
+ gatewayProcess.stderr.on('data', (d) => process.stderr.write(d));
192
+ }
193
+
194
+ const ready = await waitForReady(`http://localhost:${uiPort}`);
195
+ if (ready) {
196
+ printStatus(uiPort, gatewayPort, !!gatewayProcess, false);
197
+ }
198
+
199
+ const shutdown = (signal: string) => {
200
+ console.log(`\nShutting down (${signal})...`);
201
+ if (gatewayProcess?.pid) {
202
+ try {
203
+ process.kill(gatewayProcess.pid, 'SIGTERM');
204
+ } catch {
205
+ // ignore
206
+ }
207
+ }
57
208
  uiProcess.kill('SIGTERM');
58
209
  process.exit(0);
210
+ };
211
+
212
+ process.on('SIGINT', () => shutdown('SIGINT'));
213
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
214
+
215
+ uiProcess.on('exit', (code) => {
216
+ if (gatewayProcess?.pid) {
217
+ try {
218
+ process.kill(gatewayProcess.pid, 'SIGTERM');
219
+ } catch {
220
+ // ignore
221
+ }
222
+ }
223
+ process.exit(code ?? 0);
59
224
  });
60
225
  });
61
226
 
227
+ program
228
+ .command('stop')
229
+ .description('Stop AI Agent Router when running in background')
230
+ .action(async () => {
231
+ ensureAarDir();
232
+ let stopped = 0;
233
+ const killPidFile = (file: string, name: string) => {
234
+ if (!fs.existsSync(file)) return;
235
+ try {
236
+ const pid = parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
237
+ if (!isNaN(pid)) {
238
+ try {
239
+ process.kill(pid, 'SIGTERM');
240
+ console.log(`Stopped ${name} (PID ${pid})`);
241
+ stopped++;
242
+ } catch (e: any) {
243
+ if (e?.code !== 'ESRCH') console.warn(`${name} (PID ${pid}): ${e.message}`);
244
+ }
245
+ }
246
+ fs.unlinkSync(file);
247
+ } catch (e) {
248
+ // ignore
249
+ }
250
+ };
251
+ killPidFile(GATEWAY_PID_FILE, 'gateway');
252
+ killPidFile(UI_PID_FILE, 'Web UI');
253
+ if (fs.existsSync(GATEWAY_PORT_FILE)) fs.unlinkSync(GATEWAY_PORT_FILE);
254
+ try {
255
+ await getDatabase();
256
+ await updateServiceStatus({ status: 'stopped', pid: null });
257
+ } catch {
258
+ // ignore
259
+ }
260
+ if (stopped === 0) {
261
+ console.log('No background processes found (or already stopped).');
262
+ }
263
+ });
264
+
62
265
  program
63
266
  .command('config')
64
267
  .description('Manage gateway configuration')
@@ -8,6 +8,25 @@ const DB_PATH = process.env.DB_PATH || path.join(os.homedir(), '.aar', 'gateway.
8
8
 
9
9
  let dbInstance: Database | null = null;
10
10
  let sqlJsInstance: any = null;
11
+ /** 上次从磁盘加载 DB 的时间(用于多进程时发现磁盘被其他进程更新则重新加载) */
12
+ let lastLoadMtimeMs: number = 0;
13
+
14
+ /** 若磁盘上的 DB 被其他进程更新则重新加载,以便读到最新数据 */
15
+ function reloadFromFileIfNewer(): void {
16
+ if (!dbInstance || !fs.existsSync(DB_PATH)) return;
17
+ const mtime = fs.statSync(DB_PATH).mtimeMs;
18
+ if (mtime <= lastLoadMtimeMs) return;
19
+ try {
20
+ const fresh = loadDatabase();
21
+ if (fresh) {
22
+ dbInstance.close();
23
+ dbInstance = fresh;
24
+ lastLoadMtimeMs = fs.statSync(DB_PATH).mtimeMs;
25
+ }
26
+ } catch (e) {
27
+ console.warn('[Database] Reload from file failed:', (e as Error).message);
28
+ }
29
+ }
11
30
 
12
31
  async function initSqlJsEngine(): Promise<any> {
13
32
  if (!sqlJsInstance) {
@@ -32,6 +51,9 @@ function saveDatabase(db: Database): void {
32
51
  fs.mkdirSync(dbDir, { recursive: true });
33
52
  }
34
53
  fs.writeFileSync(DB_PATH, buffer);
54
+ if (fs.existsSync(DB_PATH)) {
55
+ lastLoadMtimeMs = fs.statSync(DB_PATH).mtimeMs;
56
+ }
35
57
  } catch (error) {
36
58
  console.error('Failed to save database:', error);
37
59
  }
@@ -53,7 +75,14 @@ export async function getDatabase(): Promise<Database> {
53
75
  if (!dbInstance) {
54
76
  const engine = await initSqlJsEngine();
55
77
 
56
- dbInstance = loadDatabase() || new engine.Database();
78
+ const loaded = loadDatabase();
79
+ if (loaded) {
80
+ dbInstance = loaded;
81
+ if (fs.existsSync(DB_PATH)) lastLoadMtimeMs = fs.statSync(DB_PATH).mtimeMs;
82
+ } else {
83
+ dbInstance = new engine.Database();
84
+ lastLoadMtimeMs = 0;
85
+ }
57
86
 
58
87
  try {
59
88
  dbInstance!.run(CREATE_TABLES_SQL);
@@ -112,9 +141,10 @@ export async function getDatabase(): Promise<Database> {
112
141
  }
113
142
  }
114
143
  }
115
-
116
- return dbInstance!;
117
- }
144
+
145
+ reloadFromFileIfNewer();
146
+ return dbInstance!;
147
+ }
118
148
 
119
149
  export async function closeDatabase(): Promise<void> {
120
150
  if (dbInstance) {
package/src/db/queries.ts CHANGED
@@ -157,10 +157,10 @@ export async function createRequestLog(log: Omit<RequestLog, 'id' | 'created_at'
157
157
  const result = await run(
158
158
  `INSERT INTO request_logs (
159
159
  model_id, request_method, request_path, request_headers,
160
- request_query, request_body, response_status, response_body, response_time_ms
161
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
160
+ request_query, request_body, response_status, response_body, response_time_ms, created_at
161
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`,
162
162
  [
163
- log.model_id || null,
163
+ log.model_id === undefined ? null : log.model_id,
164
164
  log.request_method,
165
165
  log.request_path,
166
166
  log.request_headers,
@@ -182,10 +182,10 @@ export async function createRequestLog(log: Omit<RequestLog, 'id' | 'created_at'
182
182
  const result = await run(
183
183
  `INSERT INTO request_logs (
184
184
  model_id, request_method, request_path, request_headers,
185
- request_query, request_body, response_status, response_body, response_time_ms
186
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
185
+ request_query, request_body, response_status, response_body, response_time_ms, created_at
186
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now', 'localtime'))`,
187
187
  [
188
- null,
188
+ log.model_id === undefined ? null : log.model_id,
189
189
  log.request_method,
190
190
  log.request_path,
191
191
  log.request_headers,
@@ -1,6 +1,21 @@
1
1
  import { createRequestLog } from '../db/queries';
2
2
  import { maskApiKey, maskToken } from './crypto';
3
3
 
4
+ /** Safe JSON.stringify that never throws; use for logging arbitrary response/request bodies. */
5
+ function safeStringify(value: any): string {
6
+ if (value === undefined) return '';
7
+ if (value === null) return 'null';
8
+ try {
9
+ return JSON.stringify(value);
10
+ } catch {
11
+ try {
12
+ return String(value);
13
+ } catch {
14
+ return '[Non-serializable]';
15
+ }
16
+ }
17
+ }
18
+
4
19
  export interface LogRequest {
5
20
  modelId: number | null; // Allow null for gateway requests
6
21
  method: string;
@@ -30,11 +45,11 @@ export async function logRequest(
30
45
  model_id: request.modelId,
31
46
  request_method: request.method,
32
47
  request_path: request.path,
33
- request_headers: JSON.stringify(maskedHeaders),
34
- request_query: JSON.stringify(request.query),
35
- request_body: JSON.stringify(maskedBody),
48
+ request_headers: safeStringify(maskedHeaders),
49
+ request_query: safeStringify(request.query),
50
+ request_body: safeStringify(maskedBody),
36
51
  response_status: response.status,
37
- response_body: JSON.stringify(response.body),
52
+ response_body: safeStringify(response.body),
38
53
  response_time_ms: response.responseTimeMs,
39
54
  });
40
55
  } catch (error) {