@lakphy/local-router 0.3.1 → 0.4.0-beta.1

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/cli.js CHANGED
@@ -4935,7 +4935,7 @@ var require_compile = __commonJS((exports) => {
4935
4935
  const schOrFunc = root2.refs[ref];
4936
4936
  if (schOrFunc)
4937
4937
  return schOrFunc;
4938
- let _sch = resolve5.call(this, root2, ref);
4938
+ let _sch = resolve6.call(this, root2, ref);
4939
4939
  if (_sch === undefined) {
4940
4940
  const schema = (_a21 = root2.localRefs) === null || _a21 === undefined ? undefined : _a21[ref];
4941
4941
  const { schemaId } = this.opts;
@@ -4962,7 +4962,7 @@ var require_compile = __commonJS((exports) => {
4962
4962
  function sameSchemaEnv(s1, s2) {
4963
4963
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
4964
4964
  }
4965
- function resolve5(root2, ref) {
4965
+ function resolve6(root2, ref) {
4966
4966
  let sch;
4967
4967
  while (typeof (sch = this.refs[ref]) == "string")
4968
4968
  ref = sch;
@@ -5492,7 +5492,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5492
5492
  }
5493
5493
  return uri;
5494
5494
  }
5495
- function resolve5(baseURI, relativeURI, options) {
5495
+ function resolve6(baseURI, relativeURI, options) {
5496
5496
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
5497
5497
  const resolved = resolveComponent(parse7(baseURI, schemelessOptions), parse7(relativeURI, schemelessOptions), schemelessOptions, true);
5498
5498
  schemelessOptions.skipEscape = true;
@@ -5720,7 +5720,7 @@ var require_fast_uri = __commonJS((exports, module) => {
5720
5720
  var fastUri = {
5721
5721
  SCHEMES,
5722
5722
  normalize,
5723
- resolve: resolve5,
5723
+ resolve: resolve6,
5724
5724
  resolveComponent,
5725
5725
  equal,
5726
5726
  serialize,
@@ -10440,7 +10440,7 @@ var DEFAULT_CONFIG = `{
10440
10440
  // \u65E5\u5FD7\u914D\u7F6E\uFF08\u53EF\u9009\uFF0C\u4E0D\u914D\u7F6E\u5219\u4E0D\u542F\u7528\u65E5\u5FD7\u8BB0\u5F55\uFF09
10441
10441
  // log: {
10442
10442
  // enabled: true,
10443
- // bodyPolicy: "off", // off | masked | full
10443
+ // bodyPolicy: "off", // off | full
10444
10444
  // streams: {
10445
10445
  // enabled: true,
10446
10446
  // maxBytesPerRequest: 10485760, // 10MB
@@ -10518,6 +10518,7 @@ import { parseArgs } from "util";
10518
10518
 
10519
10519
  // src/index.ts
10520
10520
  import { readFileSync as readFileSync4 } from "fs";
10521
+ import { dirname as dirname3, resolve as resolve6 } from "path";
10521
10522
 
10522
10523
  // node_modules/.bun/@ai-sdk+provider@3.0.8/node_modules/@ai-sdk/provider/dist/index.mjs
10523
10524
  var marker = "vercel.ai.error";
@@ -51823,7 +51824,6 @@ var MAX_QUERY_LIMIT = 200;
51823
51824
  var DEFAULT_QUERY_LIMIT = 50;
51824
51825
  var MAX_EXPORT_ROWS = 5000;
51825
51826
  var MAX_Q_LENGTH = 200;
51826
- var SENSITIVE_FIELD_PATTERN = /(authorization|token|cookie|password|passphrase|secret|api[_-]?key)/i;
51827
51827
  function encodeBase64Url(value) {
51828
51828
  return Buffer.from(value, "utf-8").toString("base64url");
51829
51829
  }
@@ -52039,34 +52039,6 @@ function eventToSummary(item) {
52039
52039
  sessionId: identity.sessionId
52040
52040
  };
52041
52041
  }
52042
- function maskValue(value) {
52043
- if (value == null)
52044
- return value;
52045
- if (typeof value === "string") {
52046
- return value.length > 4 ? `${value.slice(0, 2)}****` : "****";
52047
- }
52048
- if (typeof value === "number" || typeof value === "boolean") {
52049
- return "****";
52050
- }
52051
- return "****";
52052
- }
52053
- function maskSensitiveDeep(value, parentKey = "") {
52054
- if (Array.isArray(value)) {
52055
- return value.map((item) => maskSensitiveDeep(item, parentKey));
52056
- }
52057
- if (value && typeof value === "object") {
52058
- const output = {};
52059
- for (const [key2, child] of Object.entries(value)) {
52060
- if (SENSITIVE_FIELD_PATTERN.test(key2) || SENSITIVE_FIELD_PATTERN.test(parentKey)) {
52061
- output[key2] = maskValue(child);
52062
- } else {
52063
- output[key2] = maskSensitiveDeep(child, key2);
52064
- }
52065
- }
52066
- return output;
52067
- }
52068
- return value;
52069
- }
52070
52042
  function detectBodyPolicy(event) {
52071
52043
  const hasRequestBody = event.request_body !== undefined;
52072
52044
  const hasResponseBody = event.response_body !== undefined;
@@ -52139,53 +52111,61 @@ function readStreamContent(baseDir, streamFile) {
52139
52111
  }
52140
52112
  }
52141
52113
  async function buildLogEventDetail(id, parsed, location, context2) {
52142
- const maskedEvent = maskSensitiveDeep(parsed);
52143
- const level = getLevel(maskedEvent);
52144
- const statusClass = getStatusClass2(maskedEvent);
52145
- const bodyPolicy = detectBodyPolicy(maskedEvent);
52146
- const requestBodyAvailable = maskedEvent.request_body !== undefined;
52147
- const responseBodyAvailable = maskedEvent.response_body !== undefined;
52148
- const streamCaptured = Boolean(maskedEvent.stream_file);
52149
- const { content: streamContent, warning: streamWarning } = readStreamContent(resolveLogBaseDir(context2.logConfig), maskedEvent.stream_file);
52114
+ const event = parsed;
52115
+ const level = getLevel(event);
52116
+ const statusClass = getStatusClass2(event);
52117
+ const bodyPolicy = detectBodyPolicy(event);
52118
+ const requestBodyAvailable = event.request_body !== undefined;
52119
+ const responseBodyAvailable = event.response_body !== undefined;
52120
+ const streamCaptured = Boolean(event.stream_file);
52121
+ const { content: streamContent, warning: streamWarning } = readStreamContent(resolveLogBaseDir(context2.logConfig), event.stream_file);
52122
+ const hasPluginData = event.plugins_request || event.plugins_response || event.request_body_after_plugins !== undefined || event.request_url_after_plugins !== undefined || event.response_body_after_plugins !== undefined;
52123
+ const pluginsSection = hasPluginData ? {
52124
+ request: event.plugins_request,
52125
+ response: event.plugins_response,
52126
+ requestBodyAfterPlugins: event.request_body_after_plugins,
52127
+ requestUrlAfterPlugins: event.request_url_after_plugins,
52128
+ responseBodyAfterPlugins: event.response_body_after_plugins
52129
+ } : undefined;
52150
52130
  return {
52151
52131
  id,
52152
52132
  summary: {
52153
52133
  id,
52154
- ts: maskedEvent.ts_start,
52134
+ ts: event.ts_start,
52155
52135
  level,
52156
- provider: maskedEvent.provider,
52157
- routeType: maskedEvent.route_type,
52158
- routeRuleKey: maskedEvent.route_rule_key,
52159
- requestId: maskedEvent.request_id,
52160
- latencyMs: Math.max(0, maskedEvent.latency_ms ?? 0),
52161
- upstreamStatus: maskedEvent.upstream_status ?? 0,
52136
+ provider: event.provider,
52137
+ routeType: event.route_type,
52138
+ routeRuleKey: event.route_rule_key,
52139
+ requestId: event.request_id,
52140
+ latencyMs: Math.max(0, event.latency_ms ?? 0),
52141
+ upstreamStatus: event.upstream_status ?? 0,
52162
52142
  statusClass,
52163
52143
  hasError: level === "error",
52164
- model: maskedEvent.model_out || maskedEvent.model_in,
52165
- modelIn: maskedEvent.model_in,
52166
- modelOut: maskedEvent.model_out
52144
+ model: event.model_out || event.model_in,
52145
+ modelIn: event.model_in,
52146
+ modelOut: event.model_out
52167
52147
  },
52168
52148
  request: {
52169
- method: maskedEvent.method,
52170
- path: maskedEvent.path,
52171
- contentType: maskedEvent.content_type_req,
52172
- requestHeadersMasked: maskedEvent.request_headers_masked,
52173
- requestBody: maskedEvent.request_body ?? null
52149
+ method: event.method,
52150
+ path: event.path,
52151
+ contentType: event.content_type_req,
52152
+ requestHeaders: event.request_headers,
52153
+ requestBody: event.request_body ?? null
52174
52154
  },
52175
52155
  response: {
52176
- upstreamStatus: maskedEvent.upstream_status ?? 0,
52177
- contentType: maskedEvent.content_type_res,
52178
- responseHeaders: maskedEvent.response_headers,
52179
- responseBody: maskedEvent.response_body ?? null
52156
+ upstreamStatus: event.upstream_status ?? 0,
52157
+ contentType: event.content_type_res,
52158
+ responseHeaders: event.response_headers,
52159
+ responseBody: event.response_body ?? null
52180
52160
  },
52181
52161
  upstream: {
52182
- targetUrl: maskedEvent.target_url,
52183
- proxyUrl: maskedEvent.proxy_url ?? null,
52184
- providerRequestId: maskedEvent.provider_request_id,
52185
- errorType: maskedEvent.error_type,
52186
- errorMessage: maskedEvent.error_message,
52187
- isStream: maskedEvent.is_stream,
52188
- streamFile: maskedEvent.stream_file ?? null,
52162
+ targetUrl: event.target_url,
52163
+ proxyUrl: event.proxy_url ?? null,
52164
+ providerRequestId: event.provider_request_id,
52165
+ errorType: event.error_type,
52166
+ errorMessage: event.error_message,
52167
+ isStream: event.is_stream,
52168
+ streamFile: event.stream_file ?? null,
52189
52169
  streamContent
52190
52170
  },
52191
52171
  capture: {
@@ -52194,11 +52174,12 @@ async function buildLogEventDetail(id, parsed, location, context2) {
52194
52174
  responseBodyAvailable,
52195
52175
  streamCaptured,
52196
52176
  truncatedHints: [
52197
- ...buildTruncatedHints(maskedEvent, context2.logConfig?.bodyPolicy ?? bodyPolicy),
52177
+ ...buildTruncatedHints(event, context2.logConfig?.bodyPolicy ?? bodyPolicy),
52198
52178
  ...streamWarning ? [streamWarning] : []
52199
52179
  ]
52200
52180
  },
52201
- rawEvent: maskedEvent,
52181
+ ...pluginsSection && { plugins: pluginsSection },
52182
+ rawEvent: event,
52202
52183
  location
52203
52184
  };
52204
52185
  }
@@ -52910,13 +52891,6 @@ function startLogStorageBackgroundTask(logConfig) {
52910
52891
  // src/logger.ts
52911
52892
  import { appendFileSync, existsSync as existsSync6, mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
52912
52893
  import { join as join7 } from "path";
52913
- var SENSITIVE_HEADERS = new Set([
52914
- "authorization",
52915
- "x-api-key",
52916
- "cookie",
52917
- "set-cookie",
52918
- "proxy-authorization"
52919
- ]);
52920
52894
 
52921
52895
  class Logger {
52922
52896
  baseDir;
@@ -52997,14 +52971,10 @@ function getLogger() {
52997
52971
  function resetLogger() {
52998
52972
  instance = null;
52999
52973
  }
53000
- function maskHeaders(headers) {
52974
+ function collectHeaders(headers) {
53001
52975
  const result = {};
53002
52976
  headers.forEach((value, key2) => {
53003
- if (SENSITIVE_HEADERS.has(key2.toLowerCase())) {
53004
- result[key2] = value.length > 4 ? `${value.slice(0, 4)}****` : "****";
53005
- } else {
53006
- result[key2] = value;
53007
- }
52977
+ result[key2] = value;
53008
52978
  });
53009
52979
  return result;
53010
52980
  }
@@ -53016,19 +52986,8 @@ function extractProviderRequestId(headers) {
53016
52986
  }
53017
52987
  return null;
53018
52988
  }
53019
- function maskUrlCredentials(rawUrl) {
53020
- try {
53021
- const parsed = new URL(rawUrl);
53022
- if (!parsed.username && !parsed.password)
53023
- return rawUrl;
53024
- if (parsed.username)
53025
- parsed.username = "****";
53026
- if (parsed.password)
53027
- parsed.password = "****";
53028
- return parsed.toString();
53029
- } catch {
53030
- return rawUrl;
53031
- }
52989
+ function normalizeUrl(rawUrl) {
52990
+ return rawUrl;
53032
52991
  }
53033
52992
 
53034
52993
  // src/openapi.ts
@@ -53475,7 +53434,7 @@ var openAPISpec = {
53475
53434
  method: { type: "string" },
53476
53435
  path: { type: "string" },
53477
53436
  contentType: { type: ["string", "null"] },
53478
- requestHeadersMasked: {
53437
+ requestHeaders: {
53479
53438
  type: "object",
53480
53439
  additionalProperties: { type: "string" }
53481
53440
  },
@@ -54132,10 +54091,249 @@ var openAPISpec = {
54132
54091
  }
54133
54092
  };
54134
54093
 
54094
+ // src/plugin-loader.ts
54095
+ import { resolve as resolve5 } from "path";
54096
+ function isLocalPath(pkg) {
54097
+ return pkg.startsWith("./") || pkg.startsWith("../") || pkg.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pkg);
54098
+ }
54099
+ async function importPlugin(pkg, configDir) {
54100
+ let modulePath;
54101
+ if (isLocalPath(pkg)) {
54102
+ const absolutePath = resolve5(configDir, pkg);
54103
+ modulePath = `${absolutePath}?t=${Date.now()}`;
54104
+ } else {
54105
+ modulePath = pkg;
54106
+ }
54107
+ const mod = await import(modulePath);
54108
+ const definition = mod.default ?? mod;
54109
+ if (!definition || typeof definition.name !== "string" || typeof definition.create !== "function") {
54110
+ throw new Error(`\u63D2\u4EF6 "${pkg}" \u5BFC\u51FA\u683C\u5F0F\u4E0D\u6B63\u786E\uFF0C\u9700\u5BFC\u51FA\u5305\u542B name \u548C create \u7684 PluginDefinition`);
54111
+ }
54112
+ return definition;
54113
+ }
54114
+
54115
+ class PluginManager {
54116
+ plugins = new Map;
54117
+ configDir;
54118
+ constructor(configDir) {
54119
+ this.configDir = configDir;
54120
+ }
54121
+ async loadPluginsForProvider(providerName, pluginConfigs) {
54122
+ const loaded = [];
54123
+ const failures = [];
54124
+ for (const config2 of pluginConfigs) {
54125
+ try {
54126
+ const definition = await importPlugin(config2.package, this.configDir);
54127
+ const instance2 = await definition.create(config2.params ?? {});
54128
+ loaded.push({ config: config2, definition, instance: instance2 });
54129
+ } catch (err) {
54130
+ const errorMsg = err instanceof Error ? err.message : String(err);
54131
+ console.error(`[plugin] \u52A0\u8F7D\u63D2\u4EF6 "${config2.package}" \u5931\u8D25 (provider: ${providerName}):`, errorMsg);
54132
+ failures.push({ provider: providerName, package: config2.package, error: errorMsg });
54133
+ }
54134
+ }
54135
+ return { loaded, failures };
54136
+ }
54137
+ async reloadAll(providers) {
54138
+ const newPlugins = new Map;
54139
+ const allFailures = [];
54140
+ const oldPluginsToDispose = [];
54141
+ for (const [providerName, providerConfig] of Object.entries(providers)) {
54142
+ if (providerConfig.plugins && providerConfig.plugins.length > 0) {
54143
+ const { loaded, failures } = await this.loadPluginsForProvider(providerName, providerConfig.plugins);
54144
+ allFailures.push(...failures);
54145
+ if (failures.length > 0) {
54146
+ console.warn(`[plugin] provider "${providerName}" \u6709\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u4FDD\u7559\u65E7\u63D2\u4EF6\u94FE`);
54147
+ const oldLoaded = this.plugins.get(providerName);
54148
+ if (oldLoaded) {
54149
+ newPlugins.set(providerName, oldLoaded);
54150
+ }
54151
+ for (const { instance: instance2, config: config2 } of loaded) {
54152
+ try {
54153
+ await instance2.dispose?.();
54154
+ } catch (err) {
54155
+ console.error(`[plugin] \u56DE\u6EDA\u9500\u6BC1\u63D2\u4EF6 "${config2.package}" \u5931\u8D25:`, err instanceof Error ? err.message : err);
54156
+ }
54157
+ }
54158
+ } else {
54159
+ newPlugins.set(providerName, loaded);
54160
+ const oldLoaded = this.plugins.get(providerName);
54161
+ if (oldLoaded) {
54162
+ oldPluginsToDispose.push(...oldLoaded);
54163
+ }
54164
+ }
54165
+ }
54166
+ }
54167
+ for (const [providerName, oldLoaded] of this.plugins) {
54168
+ if (!newPlugins.has(providerName)) {
54169
+ oldPluginsToDispose.push(...oldLoaded);
54170
+ }
54171
+ }
54172
+ this.plugins = newPlugins;
54173
+ if (oldPluginsToDispose.length > 0) {
54174
+ setTimeout(() => {
54175
+ this.disposePluginList(oldPluginsToDispose).catch((err) => {
54176
+ console.error("[plugin] \u65E7\u63D2\u4EF6\u9500\u6BC1\u5931\u8D25:", err);
54177
+ });
54178
+ }, 5000);
54179
+ }
54180
+ if (allFailures.length > 0) {
54181
+ console.warn(`[plugin] \u70ED\u91CD\u8F7D\u5B8C\u6210\uFF0C\u4F46\u6709 ${allFailures.length} \u4E2A\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25:`, allFailures.map((f) => `${f.provider}/${f.package}`).join(", "));
54182
+ }
54183
+ return { ok: allFailures.length === 0, failures: allFailures };
54184
+ }
54185
+ getPlugins(providerName) {
54186
+ const loaded = this.plugins.get(providerName);
54187
+ if (!loaded)
54188
+ return [];
54189
+ return loaded.map((l) => l.instance);
54190
+ }
54191
+ getLoadedPlugins(providerName) {
54192
+ return this.plugins.get(providerName) ?? [];
54193
+ }
54194
+ async disposeAll() {
54195
+ const allPlugins = [];
54196
+ for (const [, loadedPlugins] of this.plugins) {
54197
+ allPlugins.push(...loadedPlugins);
54198
+ }
54199
+ this.plugins.clear();
54200
+ await this.disposePluginList(allPlugins);
54201
+ }
54202
+ async disposePluginList(plugins) {
54203
+ for (const { instance: instance2, config: config2 } of plugins) {
54204
+ try {
54205
+ await instance2.dispose?.();
54206
+ } catch (err) {
54207
+ console.error(`[plugin] \u9500\u6BC1\u63D2\u4EF6 "${config2.package}" \u5931\u8D25:`, err instanceof Error ? err.message : err);
54208
+ }
54209
+ }
54210
+ }
54211
+ async disposePluginMap(pluginMap) {
54212
+ const allPlugins = [];
54213
+ for (const [, loadedPlugins] of pluginMap) {
54214
+ allPlugins.push(...loadedPlugins);
54215
+ }
54216
+ await this.disposePluginList(allPlugins);
54217
+ }
54218
+ }
54219
+
54135
54220
  // src/proxy.ts
54136
54221
  import { appendFile, readFile, unlink } from "fs/promises";
54137
54222
  import { join as join8 } from "path";
54138
54223
  import { tmpdir } from "os";
54224
+
54225
+ // src/plugin-engine.ts
54226
+ async function executeRequestPlugins(plugins, ctx, url2, headers, body) {
54227
+ let currentUrl = url2;
54228
+ let currentHeaders = headers;
54229
+ let currentBody = body;
54230
+ for (const plugin of plugins) {
54231
+ if (!plugin.onRequest)
54232
+ continue;
54233
+ try {
54234
+ const result = await plugin.onRequest({
54235
+ ctx,
54236
+ url: currentUrl,
54237
+ headers: currentHeaders,
54238
+ body: currentBody
54239
+ });
54240
+ if (result) {
54241
+ if (result.url !== undefined)
54242
+ currentUrl = result.url;
54243
+ if (result.headers !== undefined)
54244
+ currentHeaders = result.headers;
54245
+ if (result.body !== undefined)
54246
+ currentBody = result.body;
54247
+ }
54248
+ } catch (err) {
54249
+ const error48 = err instanceof Error ? err : new Error(String(err));
54250
+ try {
54251
+ await plugin.onError?.({ ctx, phase: "request", error: error48 });
54252
+ } catch {}
54253
+ }
54254
+ }
54255
+ return { url: currentUrl, headers: currentHeaders, body: currentBody };
54256
+ }
54257
+ async function executeJsonResponsePlugins(plugins, ctx, status, headers, body) {
54258
+ let currentStatus = status;
54259
+ let currentHeaders = headers;
54260
+ let currentBody = body;
54261
+ for (let i = plugins.length - 1;i >= 0; i--) {
54262
+ const plugin = plugins[i];
54263
+ if (!plugin.onResponse)
54264
+ continue;
54265
+ try {
54266
+ const result = await plugin.onResponse({
54267
+ ctx,
54268
+ status: currentStatus,
54269
+ headers: currentHeaders,
54270
+ body: currentBody
54271
+ });
54272
+ if (result) {
54273
+ if (result.status !== undefined)
54274
+ currentStatus = result.status;
54275
+ if (result.headers !== undefined)
54276
+ currentHeaders = result.headers;
54277
+ if (result.body !== undefined)
54278
+ currentBody = result.body;
54279
+ }
54280
+ } catch (err) {
54281
+ const error48 = err instanceof Error ? err : new Error(String(err));
54282
+ try {
54283
+ await plugin.onError?.({ ctx, phase: "response", error: error48 });
54284
+ } catch {}
54285
+ }
54286
+ }
54287
+ return { status: currentStatus, headers: currentHeaders, body: currentBody };
54288
+ }
54289
+ async function createSSEPluginTransform(plugins, ctx, status, headers) {
54290
+ let currentStatus = status;
54291
+ let currentHeaders = headers;
54292
+ const transforms = [];
54293
+ for (let i = plugins.length - 1;i >= 0; i--) {
54294
+ const plugin = plugins[i];
54295
+ if (!plugin.onSSEResponse)
54296
+ continue;
54297
+ try {
54298
+ const result = await plugin.onSSEResponse({
54299
+ ctx,
54300
+ status: currentStatus,
54301
+ headers: currentHeaders
54302
+ });
54303
+ if (result) {
54304
+ if (result.status !== undefined)
54305
+ currentStatus = result.status;
54306
+ if (result.headers !== undefined)
54307
+ currentHeaders = result.headers;
54308
+ if (result.transform)
54309
+ transforms.push(result.transform);
54310
+ }
54311
+ } catch (err) {
54312
+ const error48 = err instanceof Error ? err : new Error(String(err));
54313
+ try {
54314
+ await plugin.onError?.({ ctx, phase: "response", error: error48 });
54315
+ } catch {}
54316
+ }
54317
+ }
54318
+ if (transforms.length === 0) {
54319
+ return { status: currentStatus, headers: currentHeaders, transform: null };
54320
+ }
54321
+ if (transforms.length === 1) {
54322
+ return { status: currentStatus, headers: currentHeaders, transform: transforms[0] };
54323
+ }
54324
+ const entry = new TransformStream;
54325
+ let stream = entry.readable;
54326
+ for (const t of transforms) {
54327
+ stream = stream.pipeThrough(t);
54328
+ }
54329
+ return {
54330
+ status: currentStatus,
54331
+ headers: currentHeaders,
54332
+ transform: { writable: entry.writable, readable: stream }
54333
+ };
54334
+ }
54335
+
54336
+ // src/proxy.ts
54139
54337
  var HOP_BY_HOP_HEADERS = new Set([
54140
54338
  "connection",
54141
54339
  "keep-alive",
@@ -54195,14 +54393,14 @@ function buildLogEvent(logMeta, targetUrl, proxyUrl, tsEnd, overrides) {
54195
54393
  provider: logMeta.provider,
54196
54394
  model_in: logMeta.modelIn,
54197
54395
  model_out: logMeta.modelOut,
54198
- target_url: maskUrlCredentials(targetUrl),
54199
- proxy_url: proxyUrl ? maskUrlCredentials(proxyUrl) : null,
54396
+ target_url: normalizeUrl(targetUrl),
54397
+ proxy_url: proxyUrl ? normalizeUrl(proxyUrl) : null,
54200
54398
  is_stream: logMeta.isStream,
54201
54399
  upstream_status: 0,
54202
54400
  content_type_req: logMeta.contentTypeReq,
54203
54401
  content_type_res: null,
54204
54402
  user_agent: logMeta.userAgent,
54205
- request_headers_masked: logMeta.requestHeadersMasked,
54403
+ request_headers: logMeta.requestHeaders,
54206
54404
  response_headers: {},
54207
54405
  request_bytes: logMeta.requestBytes,
54208
54406
  response_bytes: null,
@@ -54236,33 +54434,63 @@ async function flushTempCaptureToLogger(tempPath, requestId, dateStr, logger) {
54236
54434
  }
54237
54435
  }
54238
54436
  async function proxyRequest(c2, options) {
54239
- const { logMeta } = options;
54437
+ const { logMeta, plugins, pluginConfigs } = options;
54240
54438
  const logger = getLogger();
54241
54439
  const shouldLog = logger?.enabled ?? false;
54242
- const headers = buildUpstreamHeaders(c2.req.raw.headers, options.apiKey, options.authType);
54440
+ const hasPlugins = plugins && plugins.length > 0;
54441
+ let targetUrl = options.targetUrl;
54442
+ let headers = buildUpstreamHeaders(c2.req.raw.headers, options.apiKey, options.authType);
54443
+ let bodyStr = options.body;
54444
+ const pluginLogOverrides = {};
54445
+ if (hasPlugins) {
54446
+ const bodyObj = JSON.parse(bodyStr);
54447
+ const ctx = {
54448
+ requestId: logMeta.requestId,
54449
+ provider: logMeta.provider,
54450
+ modelIn: logMeta.modelIn,
54451
+ modelOut: logMeta.modelOut,
54452
+ routeType: logMeta.routeType,
54453
+ isStream: logMeta.isStream
54454
+ };
54455
+ const result = await executeRequestPlugins(plugins, ctx, targetUrl, headers, bodyObj);
54456
+ if (pluginConfigs) {
54457
+ pluginLogOverrides.plugins_request = pluginConfigs;
54458
+ }
54459
+ if (result.url !== targetUrl) {
54460
+ targetUrl = result.url;
54461
+ pluginLogOverrides.request_url_after_plugins = targetUrl;
54462
+ }
54463
+ headers = result.headers;
54464
+ const newBodyStr = JSON.stringify(result.body);
54465
+ if (newBodyStr !== bodyStr) {
54466
+ bodyStr = newBodyStr;
54467
+ pluginLogOverrides.request_body_after_plugins = result.body;
54468
+ }
54469
+ }
54243
54470
  const requestBody = shouldLog && logger?.bodyPolicy !== "off" ? JSON.parse(options.body) : undefined;
54244
54471
  const proxy = options.proxy?.trim() ? options.proxy.trim() : undefined;
54245
54472
  let upstreamRes;
54246
54473
  try {
54247
- upstreamRes = await fetch(options.targetUrl, {
54474
+ upstreamRes = await fetch(targetUrl, {
54248
54475
  method: c2.req.method,
54249
54476
  headers,
54250
- body: options.body,
54477
+ body: bodyStr,
54251
54478
  ...proxy ? { proxy } : {},
54252
54479
  decompress: true
54253
54480
  });
54254
54481
  } catch (err) {
54255
54482
  if (shouldLog) {
54256
- logger?.writeEvent(buildLogEvent(logMeta, options.targetUrl, proxy, Date.now(), {
54483
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
54257
54484
  error_type: err instanceof Error ? err.constructor.name : "UnknownError",
54258
54485
  error_message: err instanceof Error ? err.message : String(err),
54259
- ...requestBody !== undefined && { request_body: requestBody }
54486
+ ...requestBody !== undefined && { request_body: requestBody },
54487
+ ...pluginLogOverrides
54260
54488
  }));
54261
54489
  }
54262
54490
  throw err;
54263
54491
  }
54264
54492
  const responseHeaders = buildResponseHeaders(upstreamRes.headers);
54265
- if (!shouldLog) {
54493
+ if (!shouldLog && !hasPlugins) {
54266
54494
  return new Response(upstreamRes.body, {
54267
54495
  status: upstreamRes.status,
54268
54496
  headers: responseHeaders
@@ -54272,6 +54500,33 @@ async function proxyRequest(c2, options) {
54272
54500
  const providerRequestId = extractProviderRequestId(upstreamRes.headers);
54273
54501
  const dateStr = new Date(logMeta.tsStart).toISOString().slice(0, 10);
54274
54502
  if (logMeta.isStream && upstreamRes.body) {
54503
+ let sseStatus = upstreamRes.status;
54504
+ let sseHeaders = responseHeaders;
54505
+ let sseTransform = null;
54506
+ if (hasPlugins) {
54507
+ const ctx = {
54508
+ requestId: logMeta.requestId,
54509
+ provider: logMeta.provider,
54510
+ modelIn: logMeta.modelIn,
54511
+ modelOut: logMeta.modelOut,
54512
+ routeType: logMeta.routeType,
54513
+ isStream: logMeta.isStream
54514
+ };
54515
+ const sseResult = await createSSEPluginTransform(plugins, ctx, upstreamRes.status, responseHeaders);
54516
+ sseStatus = sseResult.status;
54517
+ sseHeaders = sseResult.headers;
54518
+ sseTransform = sseResult.transform;
54519
+ if (pluginConfigs) {
54520
+ pluginLogOverrides.plugins_response = pluginConfigs;
54521
+ }
54522
+ }
54523
+ if (!shouldLog) {
54524
+ const outputBody2 = sseTransform ? upstreamRes.body.pipeThrough(sseTransform) : upstreamRes.body;
54525
+ return new Response(outputBody2, {
54526
+ status: sseStatus,
54527
+ headers: sseHeaders
54528
+ });
54529
+ }
54275
54530
  const [clientStream, logStream] = upstreamRes.body.tee();
54276
54531
  (async () => {
54277
54532
  const tempPath = createTempStreamCapturePath(logMeta.requestId);
@@ -54293,30 +54548,61 @@ async function proxyRequest(c2, options) {
54293
54548
  });
54294
54549
  console.error("[logger] \u6D41\u5F0F\u65E5\u5FD7\u5904\u7406\u5931\u8D25:", err);
54295
54550
  } finally {
54296
- logger?.writeEvent(buildLogEvent(logMeta, options.targetUrl, proxy, Date.now(), {
54297
- upstream_status: upstreamRes.status,
54551
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), {
54552
+ upstream_status: sseStatus,
54298
54553
  content_type_res: contentTypeRes,
54299
- response_headers: responseHeaders,
54554
+ response_headers: sseHeaders,
54300
54555
  stream_bytes: streamBytes,
54301
54556
  provider_request_id: providerRequestId,
54302
54557
  ...streamFile != null && { stream_file: streamFile },
54303
- ...requestBody !== undefined && { request_body: requestBody }
54558
+ ...requestBody !== undefined && { request_body: requestBody },
54559
+ ...pluginLogOverrides
54304
54560
  }));
54305
54561
  }
54306
54562
  })();
54307
- return new Response(clientStream, {
54308
- status: upstreamRes.status,
54309
- headers: responseHeaders
54563
+ const outputBody = sseTransform ? clientStream.pipeThrough(sseTransform) : clientStream;
54564
+ return new Response(outputBody, {
54565
+ status: sseStatus,
54566
+ headers: sseHeaders
54567
+ });
54568
+ }
54569
+ let responseText = await upstreamRes.text();
54570
+ let responseStatus = upstreamRes.status;
54571
+ let finalResponseHeaders = responseHeaders;
54572
+ if (hasPlugins) {
54573
+ const ctx = {
54574
+ requestId: logMeta.requestId,
54575
+ provider: logMeta.provider,
54576
+ modelIn: logMeta.modelIn,
54577
+ modelOut: logMeta.modelOut,
54578
+ routeType: logMeta.routeType,
54579
+ isStream: logMeta.isStream
54580
+ };
54581
+ const result = await executeJsonResponsePlugins(plugins, ctx, upstreamRes.status, responseHeaders, responseText);
54582
+ if (pluginConfigs) {
54583
+ pluginLogOverrides.plugins_response = pluginConfigs;
54584
+ }
54585
+ if (result.body !== responseText) {
54586
+ pluginLogOverrides.response_body_after_plugins = result.body;
54587
+ }
54588
+ responseStatus = result.status;
54589
+ finalResponseHeaders = result.headers;
54590
+ responseText = result.body;
54591
+ }
54592
+ if (!shouldLog) {
54593
+ return new Response(responseText, {
54594
+ status: responseStatus,
54595
+ headers: finalResponseHeaders
54310
54596
  });
54311
54597
  }
54312
- const responseText = await upstreamRes.text();
54313
54598
  const responseBytes = Buffer.byteLength(responseText, "utf-8");
54314
54599
  const eventOverrides = {
54315
54600
  upstream_status: upstreamRes.status,
54316
54601
  content_type_res: contentTypeRes,
54317
- response_headers: responseHeaders,
54602
+ response_headers: finalResponseHeaders,
54318
54603
  response_bytes: responseBytes,
54319
- provider_request_id: providerRequestId
54604
+ provider_request_id: providerRequestId,
54605
+ ...pluginLogOverrides
54320
54606
  };
54321
54607
  if (requestBody !== undefined) {
54322
54608
  eventOverrides.request_body = requestBody;
@@ -54324,10 +54610,10 @@ async function proxyRequest(c2, options) {
54324
54610
  if (logger?.bodyPolicy !== "off") {
54325
54611
  eventOverrides.response_body = responseText;
54326
54612
  }
54327
- logger?.writeEvent(buildLogEvent(logMeta, options.targetUrl, proxy, Date.now(), eventOverrides));
54613
+ logger?.writeEvent(buildLogEvent(logMeta, targetUrl, proxy, Date.now(), eventOverrides));
54328
54614
  return new Response(responseText, {
54329
- status: upstreamRes.status,
54330
- headers: responseHeaders
54615
+ status: responseStatus,
54616
+ headers: finalResponseHeaders
54331
54617
  });
54332
54618
  }
54333
54619
 
@@ -54342,7 +54628,7 @@ function resolveRoute(modelMap, incomingModel) {
54342
54628
  return;
54343
54629
  }
54344
54630
  function createModelRoutingHandler(options) {
54345
- const { routeType, store, authType, buildTargetUrl } = options;
54631
+ const { routeType, store, authType, buildTargetUrl, pluginManager } = options;
54346
54632
  return async (c2) => {
54347
54633
  const config2 = store.get();
54348
54634
  const modelMap = config2.routes[routeType];
@@ -54382,51 +54668,62 @@ function createModelRoutingHandler(options) {
54382
54668
  contentTypeReq: c2.req.header("content-type") ?? null,
54383
54669
  userAgent: c2.req.header("user-agent") ?? null,
54384
54670
  requestBytes: Buffer.byteLength(body, "utf-8"),
54385
- requestHeadersMasked: maskHeaders(c2.req.raw.headers)
54671
+ requestHeaders: collectHeaders(c2.req.raw.headers)
54386
54672
  };
54673
+ const plugins = pluginManager?.getPlugins(target.provider) ?? [];
54674
+ const pluginConfigs = pluginManager?.getLoadedPlugins(target.provider) ?? [];
54387
54675
  return proxyRequest(c2, {
54388
54676
  targetUrl,
54389
54677
  apiKey: provider.apiKey,
54390
54678
  proxy: provider.proxy,
54391
54679
  authType,
54392
54680
  body,
54393
- logMeta
54681
+ logMeta,
54682
+ plugins: plugins.length > 0 ? plugins : undefined,
54683
+ pluginConfigs: pluginConfigs.length > 0 ? pluginConfigs.map((lp) => ({
54684
+ name: lp.definition.name,
54685
+ package: lp.config.package,
54686
+ params: lp.config.params ?? {}
54687
+ })) : undefined
54394
54688
  });
54395
54689
  };
54396
54690
  }
54397
54691
 
54398
54692
  // src/routes/anthropic-messages.ts
54399
- function createAnthropicMessagesRoutes(routeType, store) {
54693
+ function createAnthropicMessagesRoutes(routeType, store, pluginManager) {
54400
54694
  const routes = new Hono2;
54401
54695
  routes.post("/v1/messages", createModelRoutingHandler({
54402
54696
  routeType,
54403
54697
  store,
54404
54698
  authType: "x-api-key",
54405
- buildTargetUrl: (base) => `${base}/v1/messages`
54699
+ buildTargetUrl: (base) => `${base}/v1/messages`,
54700
+ pluginManager
54406
54701
  }));
54407
54702
  return routes;
54408
54703
  }
54409
54704
 
54410
54705
  // src/routes/openai-completions.ts
54411
- function createOpenaiCompletionsRoutes(routeType, store) {
54706
+ function createOpenaiCompletionsRoutes(routeType, store, pluginManager) {
54412
54707
  const routes = new Hono2;
54413
54708
  routes.post("/v1/chat/completions", createModelRoutingHandler({
54414
54709
  routeType,
54415
54710
  store,
54416
54711
  authType: "bearer",
54417
- buildTargetUrl: (base) => `${base}/v1/chat/completions`
54712
+ buildTargetUrl: (base) => `${base}/v1/chat/completions`,
54713
+ pluginManager
54418
54714
  }));
54419
54715
  return routes;
54420
54716
  }
54421
54717
 
54422
54718
  // src/routes/openai-responses.ts
54423
- function createOpenaiResponsesRoutes(routeType, store) {
54719
+ function createOpenaiResponsesRoutes(routeType, store, pluginManager) {
54424
54720
  const routes = new Hono2;
54425
54721
  routes.post("/v1/responses", createModelRoutingHandler({
54426
54722
  routeType,
54427
54723
  store,
54428
54724
  authType: "bearer",
54429
- buildTargetUrl: (base) => `${base}/v1/responses`
54725
+ buildTargetUrl: (base) => `${base}/v1/responses`,
54726
+ pluginManager
54430
54727
  }));
54431
54728
  return routes;
54432
54729
  }
@@ -54587,7 +54884,7 @@ function createChatProxyModel(providerName, providerConfig, model) {
54587
54884
  throw new Error(`\u6682\u4E0D\u652F\u6301\u7684 provider \u7C7B\u578B: ${providerConfig.type}`);
54588
54885
  }
54589
54886
  }
54590
- function createAdminApiRoutes(store, registerCleanup) {
54887
+ function createAdminApiRoutes(store, pluginManager, registerCleanup) {
54591
54888
  const api2 = new Hono2;
54592
54889
  const cryptoSessions = new Map;
54593
54890
  const CRYPTO_SESSION_TTL_MS = 2 * 60 * 1000;
@@ -54698,18 +54995,22 @@ function createAdminApiRoutes(store, registerCleanup) {
54698
54995
  session.dispose();
54699
54996
  }
54700
54997
  });
54701
- api2.post("/config/apply", (_c) => {
54998
+ api2.post("/config/apply", async (_c) => {
54702
54999
  try {
54703
55000
  const config2 = store.reload();
54704
55001
  if (config2.log) {
54705
55002
  const logBaseDir = resolveLogBaseDir(config2.log);
54706
55003
  initLogger(logBaseDir, config2.log);
54707
55004
  }
55005
+ const pluginResult = await pluginManager.reloadAll(config2.providers);
54708
55006
  return _c.json({
54709
55007
  ok: true,
54710
55008
  summary: {
54711
55009
  providers: Object.keys(config2.providers).length,
54712
55010
  routes: Object.keys(config2.routes).length
55011
+ },
55012
+ ...pluginResult.failures.length > 0 && {
55013
+ pluginWarnings: pluginResult.failures
54713
55014
  }
54714
55015
  });
54715
55016
  } catch (err) {
@@ -55134,7 +55435,7 @@ async function proxyAdminToDevServer(c2, origin) {
55134
55435
  headers: buildProxyResponseHeaders(upstreamRes.headers)
55135
55436
  });
55136
55437
  }
55137
- function createApp(store, options) {
55438
+ async function createApp(store, options) {
55138
55439
  const config2 = store.get();
55139
55440
  console.log(`\u5DF2\u52A0\u8F7D\u914D\u7F6E: ${store.getPath()}`);
55140
55441
  if (config2.log) {
@@ -55145,15 +55446,24 @@ function createApp(store, options) {
55145
55446
  }
55146
55447
  const stopLogStorageTask = startLogStorageBackgroundTask(config2.log);
55147
55448
  options?.registerCleanup?.(stopLogStorageTask);
55449
+ const configDir = dirname3(resolve6(store.getPath()));
55450
+ const pluginManager = new PluginManager(configDir);
55451
+ const reloadResult = await pluginManager.reloadAll(config2.providers);
55452
+ if (!reloadResult.ok) {
55453
+ console.warn(`[plugin] \u63D2\u4EF6\u521D\u59CB\u5316\u5B8C\u6210\uFF0C\u4F46\u6709 ${reloadResult.failures.length} \u4E2A\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25`);
55454
+ }
55455
+ options?.registerCleanup?.(() => {
55456
+ pluginManager.disposeAll().catch(() => {});
55457
+ });
55148
55458
  printIntegrationGuide(config2);
55149
55459
  const app = new Hono2;
55150
55460
  app.get("/", (c2) => c2.text("local-router is running"));
55151
55461
  for (const [routeType, entry] of Object.entries(ROUTE_REGISTRY)) {
55152
- const subApp = entry.create(routeType, store);
55462
+ const subApp = entry.create(routeType, store, pluginManager);
55153
55463
  app.route(entry.mountPrefix, subApp);
55154
55464
  console.log(`\u5DF2\u6CE8\u518C\u8DEF\u7531: ${routeType} -> ${entry.mountPrefix}`);
55155
55465
  }
55156
- app.route("/api", createAdminApiRoutes(store, options?.registerCleanup));
55466
+ app.route("/api", createAdminApiRoutes(store, pluginManager, options?.registerCleanup));
55157
55467
  console.log("\u5DF2\u6CE8\u518C\u7BA1\u7406 API: /api");
55158
55468
  app.get("/api/docs", middleware({ url: "/api/openapi.json" }));
55159
55469
  app.get("/api/openapi.json", (c2) => c2.json(openAPISpec));
@@ -55182,10 +55492,10 @@ function createApp(store, options) {
55182
55492
  }
55183
55493
  return app;
55184
55494
  }
55185
- function createAppRuntimeFromConfigPath(configPath) {
55495
+ async function createAppRuntimeFromConfigPath(configPath) {
55186
55496
  const store = new ConfigStore(configPath);
55187
55497
  const cleanups = [];
55188
- const app = createApp(store, {
55498
+ const app = await createApp(store, {
55189
55499
  registerCleanup: (cleanup) => {
55190
55500
  cleanups.push(cleanup);
55191
55501
  }
@@ -55203,12 +55513,29 @@ function createAppRuntimeFromConfigPath(configPath) {
55203
55513
  }
55204
55514
 
55205
55515
  // src/server.ts
55206
- function startServer(options) {
55207
- const runtime = createAppRuntimeFromConfigPath(options.configPath);
55516
+ var DEFAULT_IDLE_TIMEOUT_SECONDS = 600;
55517
+ function resolveIdleTimeoutSeconds(explicit) {
55518
+ if (typeof explicit === "number" && Number.isFinite(explicit) && explicit >= 0) {
55519
+ return explicit;
55520
+ }
55521
+ const fromEnv = process.env.LOCAL_ROUTER_IDLE_TIMEOUT;
55522
+ if (!fromEnv) {
55523
+ return DEFAULT_IDLE_TIMEOUT_SECONDS;
55524
+ }
55525
+ const parsed = Number.parseInt(fromEnv, 10);
55526
+ if (Number.isFinite(parsed) && parsed >= 0) {
55527
+ return parsed;
55528
+ }
55529
+ return DEFAULT_IDLE_TIMEOUT_SECONDS;
55530
+ }
55531
+ async function startServer(options) {
55532
+ const runtime = await createAppRuntimeFromConfigPath(options.configPath);
55533
+ const idleTimeout = resolveIdleTimeoutSeconds(options.idleTimeoutSeconds);
55208
55534
  const server = Bun.serve({
55209
55535
  fetch: runtime.app.fetch,
55210
55536
  hostname: options.host,
55211
- port: options.port
55537
+ port: options.port,
55538
+ idleTimeout
55212
55539
  });
55213
55540
  const host = server.hostname;
55214
55541
  const port = server.port;
@@ -55227,7 +55554,7 @@ function startServer(options) {
55227
55554
  // src/cli/runtime.ts
55228
55555
  import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync, writeFileSync as writeFileSync4 } from "fs";
55229
55556
  import { homedir as homedir2 } from "os";
55230
- import { join as join9, resolve as resolve5 } from "path";
55557
+ import { join as join9, resolve as resolve7 } from "path";
55231
55558
  function getRuntimeDirs() {
55232
55559
  const root2 = join9(homedir2(), ".local-router");
55233
55560
  return {
@@ -55274,7 +55601,7 @@ function clearRuntimeFiles() {
55274
55601
  rmSync(files.state, { force: true });
55275
55602
  }
55276
55603
  function resolveConfigArgPath(pathValue) {
55277
- return resolve5(pathValue);
55604
+ return resolve7(pathValue);
55278
55605
  }
55279
55606
 
55280
55607
  // src/cli/process.ts
@@ -55284,7 +55611,8 @@ function parseSharedFlags(args) {
55284
55611
  options: {
55285
55612
  config: { type: "string" },
55286
55613
  host: { type: "string" },
55287
- port: { type: "string" }
55614
+ port: { type: "string" },
55615
+ "idle-timeout": { type: "string" }
55288
55616
  },
55289
55617
  allowPositionals: true,
55290
55618
  strict: false
@@ -55294,10 +55622,16 @@ function parseSharedFlags(args) {
55294
55622
  if (portRaw && !Number.isFinite(port)) {
55295
55623
  throw new Error(`\u65E0\u6548\u7AEF\u53E3: ${portRaw}`);
55296
55624
  }
55625
+ const idleTimeoutRaw = parsed.values["idle-timeout"];
55626
+ const idleTimeoutSeconds = idleTimeoutRaw ? Number.parseInt(idleTimeoutRaw, 10) : undefined;
55627
+ if (idleTimeoutRaw && (!Number.isFinite(idleTimeoutSeconds) || idleTimeoutSeconds < 0)) {
55628
+ throw new Error(`\u65E0\u6548 idle-timeout: ${idleTimeoutRaw}`);
55629
+ }
55297
55630
  return {
55298
55631
  config: parsed.values.config,
55299
55632
  host: parsed.values.host,
55300
- port
55633
+ port,
55634
+ idleTimeoutSeconds
55301
55635
  };
55302
55636
  }
55303
55637
  async function checkHealth(baseUrl, timeoutMs = 1500) {
@@ -55343,12 +55677,14 @@ async function runServerProcess(opts) {
55343
55677
  if (!Number.isFinite(port)) {
55344
55678
  throw new Error(`\u65E0\u6548\u7AEF\u53E3: ${opts.port ?? process.env.PORT}`);
55345
55679
  }
55680
+ const idleTimeoutSeconds = opts.idleTimeoutSeconds ?? Number.parseInt(process.env.LOCAL_ROUTER_IDLE_TIMEOUT ?? "", 10);
55346
55681
  let running;
55347
55682
  try {
55348
- running = startServer({
55683
+ running = await startServer({
55349
55684
  configPath: ensured.path,
55350
55685
  host,
55351
- port
55686
+ port,
55687
+ idleTimeoutSeconds: Number.isFinite(idleTimeoutSeconds) ? idleTimeoutSeconds : undefined
55352
55688
  });
55353
55689
  } catch (err) {
55354
55690
  const code = err?.code;
@@ -55422,6 +55758,9 @@ async function startDaemon(flags) {
55422
55758
  if (typeof flags.port === "number") {
55423
55759
  childArgs.push("--port", String(flags.port));
55424
55760
  }
55761
+ if (typeof flags.idleTimeoutSeconds === "number") {
55762
+ childArgs.push("--idle-timeout", String(flags.idleTimeoutSeconds));
55763
+ }
55425
55764
  childArgs.push("--log-file", files.daemonLog);
55426
55765
  const child = Bun.spawn([process.execPath, ...childArgs], {
55427
55766
  stdin: "ignore",
@@ -55493,7 +55832,7 @@ function readLogDelta(filePath, offset) {
55493
55832
  // src/cli/config-command.ts
55494
55833
  import { createInterface as createInterface4 } from "readline/promises";
55495
55834
  import { mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
55496
- import { dirname as dirname2, join as join10 } from "path";
55835
+ import { dirname as dirname4, join as join10 } from "path";
55497
55836
  import { parseArgs as parseArgs2 } from "util";
55498
55837
  function readConfig(configArg) {
55499
55838
  const path = resolveConfigPath(configArg);
@@ -55501,7 +55840,7 @@ function readConfig(configArg) {
55501
55840
  }
55502
55841
  function saveConfig(path, config2) {
55503
55842
  validateConfigOrThrow(config2);
55504
- const backupDir = join10(dirname2(path), ".backups");
55843
+ const backupDir = join10(dirname4(path), ".backups");
55505
55844
  mkdirSync4(backupDir, { recursive: true });
55506
55845
  const backupPath = join10(backupDir, `config-${Date.now()}.json5`);
55507
55846
  writeFileSync5(backupPath, readFileSync7(path, "utf-8"), "utf-8");
@@ -55947,9 +56286,9 @@ Usage:
55947
56286
  local-router <command> [options]
55948
56287
 
55949
56288
  Commands:
55950
- start [--daemon] [--config <path>] [--host <host>] [--port <port>]
56289
+ start [--daemon] [--config <path>] [--host <host>] [--port <port>] [--idle-timeout <sec>]
55951
56290
  stop
55952
- restart [--daemon] [--config <path>] [--host <host>] [--port <port>]
56291
+ restart [--daemon] [--config <path>] [--host <host>] [--port <port>] [--idle-timeout <sec>]
55953
56292
  status [--json]
55954
56293
  logs [--follow] [--lines <n>]
55955
56294
  init [--config <path>] [--force]
@@ -55959,7 +56298,7 @@ Commands:
55959
56298
  version
55960
56299
 
55961
56300
  Hidden commands:
55962
- __run-server --mode <daemon|foreground> [--config] [--host] [--port] [--log-file]
56301
+ __run-server --mode <daemon|foreground> [--config] [--host] [--port] [--idle-timeout] [--log-file]
55963
56302
  `);
55964
56303
  }
55965
56304
  async function printVersion() {
@@ -55989,7 +56328,8 @@ async function cmdStart(args) {
55989
56328
  mode: "foreground",
55990
56329
  config: flags.config,
55991
56330
  host: flags.host,
55992
- port: flags.port
56331
+ port: flags.port,
56332
+ idleTimeoutSeconds: flags.idleTimeoutSeconds
55993
56333
  });
55994
56334
  }
55995
56335
  async function cmdStop() {
@@ -56188,6 +56528,7 @@ async function cmdRunServer(args) {
56188
56528
  config: { type: "string" },
56189
56529
  host: { type: "string" },
56190
56530
  port: { type: "string" },
56531
+ "idle-timeout": { type: "string" },
56191
56532
  "log-file": { type: "string" }
56192
56533
  },
56193
56534
  allowPositionals: true,
@@ -56196,11 +56537,14 @@ async function cmdRunServer(args) {
56196
56537
  const mode = parsed.values.mode === "daemon" ? "daemon" : "foreground";
56197
56538
  const portRaw = parsed.values.port;
56198
56539
  const port = portRaw ? Number.parseInt(portRaw, 10) : undefined;
56540
+ const idleTimeoutRaw = parsed.values["idle-timeout"];
56541
+ const idleTimeoutSeconds = idleTimeoutRaw ? Number.parseInt(idleTimeoutRaw, 10) : undefined;
56199
56542
  await runServerProcess({
56200
56543
  mode,
56201
56544
  config: parsed.values.config,
56202
56545
  host: parsed.values.host,
56203
56546
  port,
56547
+ idleTimeoutSeconds: Number.isFinite(idleTimeoutSeconds) ? idleTimeoutSeconds : undefined,
56204
56548
  logFile: parsed.values["log-file"]
56205
56549
  });
56206
56550
  }