@vtstech/pi-shared 1.1.5 → 1.1.7

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/README.md CHANGED
@@ -8,9 +8,12 @@ This is an internal dependency — you don't need to install it directly. It's p
8
8
 
9
9
  | Module | Description |
10
10
  |--------|-------------|
11
+ | `debug` | Conditional debug logging via `PI_EXTENSIONS_DEBUG=1` env var — `debugLog(module, message, ...args)` |
11
12
  | `format` | Section headers, indicators (ok/fail/warn/info), numeric formatters (bytes, ms, percentages), string utilities |
12
- | `ollama` | Ollama base URL resolution, models.json I/O with TTL cache, model family detection, provider detection, Ollama API helpers |
13
- | `security` | Security mode toggle (`basic`/`max`), partitioned command blocklist (41 CRITICAL + 25 EXTENDED), mode-aware SSRF (19 + 7 patterns), path validation with symlink dereference, URL validation, command sanitization, audit logging with mode tracking (`AUDIT_LOG_PATH` exported) |
13
+ | `model-test-utils` | Shared test utilities — `ChatFn` abstraction, unified test functions, scoring helpers, tool support cache, user config (`~/.pi/agent/model-test-config.json`), test history with regression detection (`~/.pi/agent/cache/model-test-history.json`) |
14
+ | `ollama` | Ollama base URL resolution, models.json I/O with TTL cache, async write mutex (`acquireModelsJsonLock`, `readModifyWriteModelsJson`), exponential backoff retry (`withRetry`), model family detection, provider detection, Ollama API helpers |
15
+ | `react-parser` | Multi-dialect ReAct text parser — 4 dialects (react, function, tool, call), `parseReact()`, `detectReactDialect()`, `fuzzyMatchToolName()` |
16
+ | `security` | Security mode toggle (`basic`/`max`), partitioned command blocklist (41 CRITICAL + 25 EXTENDED) with full-word scanning, mode-aware SSRF (22 + 7 patterns), path validation with symlink dereference, URL validation, command sanitization, DNS rebinding protection (`resolveAndCheckHostname`), buffered audit logging with mode tracking (`AUDIT_LOG_PATH` exported) |
14
17
  | `types` | Type definitions (ToolSupportLevel, AuditEntry, etc.) |
15
18
 
16
19
  ## Usage
@@ -27,4 +30,4 @@ import { readModelsJson, getOllamaBaseUrl } from "@vtstech/pi-shared/ollama";
27
30
 
28
31
  ## License
29
32
 
30
- MIT — [VTSTech](https://www.vts-tech.org)
33
+ MIT — [VTSTech](https://www.vts-tech.org)
@@ -38,6 +38,34 @@ var CONFIG = {
38
38
  TEST_DELAY_MS: 1e4
39
39
  // 10 seconds between tests to avoid rate limiting
40
40
  };
41
+ var TEST_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent");
42
+ var TEST_CONFIG_PATH = path.join(TEST_CONFIG_DIR, "model-test-config.json");
43
+ function readTestConfig() {
44
+ try {
45
+ if (fs.existsSync(TEST_CONFIG_PATH)) {
46
+ const raw = fs.readFileSync(TEST_CONFIG_PATH, "utf-8");
47
+ return JSON.parse(raw);
48
+ }
49
+ } catch {
50
+ }
51
+ return {};
52
+ }
53
+ function getEffectiveConfig() {
54
+ const userConfig = readTestConfig();
55
+ return {
56
+ ...CONFIG,
57
+ DEFAULT_TIMEOUT_MS: userConfig.defaultTimeoutMs ?? CONFIG.DEFAULT_TIMEOUT_MS,
58
+ CONNECT_TIMEOUT_S: userConfig.connectTimeoutS ?? CONFIG.CONNECT_TIMEOUT_S,
59
+ MAX_RETRIES: userConfig.maxRetries ?? CONFIG.MAX_RETRIES,
60
+ RETRY_DELAY_MS: userConfig.retryDelayMs ?? CONFIG.RETRY_DELAY_MS,
61
+ TEST_DELAY_MS: userConfig.testDelayMs ?? CONFIG.TEST_DELAY_MS,
62
+ TOOL_TEST_TIMEOUT_MS: userConfig.toolTestTimeoutMs ?? CONFIG.TOOL_TEST_TIMEOUT_MS,
63
+ PROVIDER_TIMEOUT_MS: userConfig.providerTimeoutMs ?? CONFIG.PROVIDER_TIMEOUT_MS,
64
+ PROVIDER_TOOL_TIMEOUT_MS: userConfig.providerToolTimeoutMs ?? CONFIG.PROVIDER_TOOL_TIMEOUT_MS,
65
+ NUM_PREDICT: userConfig.numPredict ?? CONFIG.NUM_PREDICT,
66
+ TEMPERATURE: userConfig.temperature ?? CONFIG.TEMPERATURE
67
+ };
68
+ }
41
69
  var WEATHER_TOOL_DEFINITION = {
42
70
  type: "function",
43
71
  function: {
@@ -150,6 +178,91 @@ function cacheToolSupport(model, support, family) {
150
178
  _toolSupportCacheInMemory = cache;
151
179
  writeToolSupportCache(cache);
152
180
  }
181
+ var TEST_HISTORY_DIR = path.join(os.homedir(), ".pi", "agent", "cache");
182
+ var TEST_HISTORY_PATH = path.join(TEST_HISTORY_DIR, "model-test-history.json");
183
+ var MAX_HISTORY_PER_MODEL = 50;
184
+ var MAX_HISTORY_TOTAL = 500;
185
+ function readTestHistory() {
186
+ try {
187
+ if (fs.existsSync(TEST_HISTORY_PATH)) {
188
+ const raw = fs.readFileSync(TEST_HISTORY_PATH, "utf-8");
189
+ return JSON.parse(raw);
190
+ }
191
+ } catch {
192
+ }
193
+ return {};
194
+ }
195
+ function writeTestHistory(history) {
196
+ for (const model of Object.keys(history)) {
197
+ if (history[model].length > MAX_HISTORY_PER_MODEL) {
198
+ history[model] = history[model].slice(-MAX_HISTORY_PER_MODEL);
199
+ }
200
+ }
201
+ let totalEntries = 0;
202
+ const modelsByRecency = Object.entries(history).map(([model, entries]) => ({
203
+ model,
204
+ entries,
205
+ lastEntry: entries[entries.length - 1]?.timestamp || ""
206
+ })).sort((a, b) => b.lastEntry.localeCompare(a.lastEntry));
207
+ const trimmedHistory = {};
208
+ for (const { model, entries } of modelsByRecency) {
209
+ if (totalEntries + entries.length > MAX_HISTORY_TOTAL) {
210
+ const remaining = MAX_HISTORY_TOTAL - totalEntries;
211
+ if (remaining <= 0) break;
212
+ trimmedHistory[model] = entries.slice(-remaining);
213
+ totalEntries += remaining;
214
+ } else {
215
+ trimmedHistory[model] = entries;
216
+ totalEntries += entries.length;
217
+ }
218
+ }
219
+ if (!fs.existsSync(TEST_HISTORY_DIR)) {
220
+ fs.mkdirSync(TEST_HISTORY_DIR, { recursive: true });
221
+ }
222
+ fs.writeFileSync(TEST_HISTORY_PATH, JSON.stringify(trimmedHistory, null, 2) + "\n", "utf-8");
223
+ }
224
+ function appendTestHistory(entry) {
225
+ const history = readTestHistory();
226
+ if (!history[entry.model]) {
227
+ history[entry.model] = [];
228
+ }
229
+ history[entry.model].push(entry);
230
+ writeTestHistory(history);
231
+ }
232
+ function getModelHistory(model, limit = 10) {
233
+ const history = readTestHistory();
234
+ const entries = history[model] || [];
235
+ return entries.slice(-limit);
236
+ }
237
+ function detectRegression(model, current) {
238
+ const history = readTestHistory();
239
+ const entries = history[model] || [];
240
+ if (entries.length < 2) return [];
241
+ const previous = entries[entries.length - 2];
242
+ const regressions = [];
243
+ const scoreOrder = ["STRONG", "MODERATE", "WEAK", "FAIL", "ERROR", "NO", "YES"];
244
+ const scoreRank = (s) => {
245
+ const idx = scoreOrder.indexOf(s);
246
+ return idx >= 0 ? idx : 99;
247
+ };
248
+ if (scoreRank(current.tests.reasoning.score) > scoreRank(previous.tests.reasoning.score)) {
249
+ regressions.push({ test: "Reasoning", previous: previous.tests.reasoning.score, current: current.tests.reasoning.score });
250
+ }
251
+ if (scoreRank(current.tests.toolUsage.score) > scoreRank(previous.tests.toolUsage.score)) {
252
+ regressions.push({ test: "Tool Usage", previous: previous.tests.toolUsage.score, current: current.tests.toolUsage.score });
253
+ }
254
+ if (scoreRank(current.tests.reactParsing.score) > scoreRank(previous.tests.reactParsing.score)) {
255
+ regressions.push({ test: "ReAct Parsing", previous: previous.tests.reactParsing.score, current: current.tests.reactParsing.score });
256
+ }
257
+ if (scoreRank(current.tests.instructionFollowing.score) > scoreRank(previous.tests.instructionFollowing.score)) {
258
+ regressions.push({ test: "Instructions", previous: previous.tests.instructionFollowing.score, current: current.tests.instructionFollowing.score });
259
+ }
260
+ const supportRank = (s) => s === "native" ? 0 : s === "react" ? 1 : 2;
261
+ if (supportRank(current.tests.toolSupport.level) > supportRank(previous.tests.toolSupport.level)) {
262
+ regressions.push({ test: "Tool Support", previous: previous.tests.toolSupport.level, current: current.tests.toolSupport.level });
263
+ }
264
+ return regressions;
265
+ }
153
266
  var REASONING_PROMPT = `A snail climbs 3 feet up a wall each day, but slides back 2 feet each night. The wall is 10 feet tall. How many days does it take the snail to reach the top? Think step by step and give the final answer on its own line like: ANSWER: <number>`;
154
267
  var TOOL_SYSTEM_PROMPT = "You are a helpful assistant. Use the available tools when needed.";
155
268
  var TOOL_USER_PROMPT = "What's the weather like in Paris right now?";
@@ -309,11 +422,18 @@ The JSON object must have exactly these 4 keys:
309
422
  }
310
423
  export {
311
424
  CONFIG,
425
+ TEST_CONFIG_PATH,
312
426
  TOOL_SUPPORT_CACHE_PATH,
313
427
  WEATHER_TOOL_DEFINITION,
428
+ appendTestHistory,
314
429
  cacheToolSupport,
430
+ detectRegression,
315
431
  getCachedToolSupport,
432
+ getEffectiveConfig,
433
+ getModelHistory,
316
434
  parseTextToolCall,
435
+ readTestConfig,
436
+ readTestHistory,
317
437
  readToolSupportCache,
318
438
  scoreNativeToolCall,
319
439
  scoreReasoning,
@@ -321,5 +441,6 @@ export {
321
441
  testInstructionFollowingUnified,
322
442
  testReasoningUnified,
323
443
  testToolUsageUnified,
444
+ writeTestHistory,
324
445
  writeToolSupportCache
325
446
  };
package/ollama.js CHANGED
@@ -12,7 +12,7 @@ function debugLog(module, message, ...args) {
12
12
  }
13
13
 
14
14
  // shared/ollama.ts
15
- var EXTENSION_VERSION = "1.1.5";
15
+ var EXTENSION_VERSION = "1.1.7";
16
16
  var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
17
17
  var _modelsJsonCache = null;
18
18
  var _ollamaBaseUrlCache = null;
@@ -71,35 +71,119 @@ function writeModelsJson(data) {
71
71
  _modelsJsonCache = null;
72
72
  _ollamaBaseUrlCache = null;
73
73
  }
74
- async function fetchOllamaModels(baseUrl) {
75
- const res = await fetch(`${baseUrl}/api/tags`, {
76
- signal: AbortSignal.timeout(5e3)
74
+ var _modelsJsonLock = null;
75
+ async function acquireModelsJsonLock() {
76
+ while (_modelsJsonLock) {
77
+ await _modelsJsonLock;
78
+ }
79
+ let releaseLock;
80
+ _modelsJsonLock = new Promise((resolve) => {
81
+ releaseLock = resolve;
77
82
  });
78
- if (!res.ok) throw new Error(`Ollama returned ${res.status}`);
79
- const data = await res.json();
80
- return data.models ?? [];
83
+ return {
84
+ release: () => {
85
+ releaseLock();
86
+ _modelsJsonLock = null;
87
+ }
88
+ };
81
89
  }
82
- async function fetchModelContextLength(baseUrl, modelName) {
90
+ async function readModifyWriteModelsJson(modifier) {
91
+ const { release } = await acquireModelsJsonLock();
83
92
  try {
84
- const res = await fetch(`${baseUrl}/api/show`, {
85
- method: "POST",
86
- headers: { "Content-Type": "application/json" },
87
- body: JSON.stringify({ name: modelName }),
88
- signal: AbortSignal.timeout(3e4)
93
+ const data = readModelsJson();
94
+ const modified = modifier(data);
95
+ if (modified === null) return false;
96
+ writeModelsJson(modified);
97
+ return true;
98
+ } finally {
99
+ release();
100
+ }
101
+ }
102
+ var DEFAULT_RETRY_OPTIONS = {
103
+ maxRetries: 2,
104
+ baseDelayMs: 1e3,
105
+ maxDelayMs: 1e4,
106
+ retryOnTimeout: true,
107
+ retryOnConnectionError: true
108
+ };
109
+ function backoffDelay(attempt, baseDelayMs, maxDelayMs) {
110
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
111
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
112
+ return Math.max(0, Math.round(delay + jitter));
113
+ }
114
+ var RETRYABLE_ERROR_PATTERNS = [
115
+ "ECONNREFUSED",
116
+ "ECONNRESET",
117
+ "ENOTFOUND",
118
+ "ETIMEDOUT",
119
+ "fetch failed",
120
+ "network error",
121
+ "socket hang up",
122
+ "Empty response"
123
+ ];
124
+ function isRetryableError(error, opts) {
125
+ if (error instanceof Error) {
126
+ if (error.name === "AbortError" && opts.retryOnTimeout) return true;
127
+ const msg = error.message;
128
+ if (opts.retryOnConnectionError && RETRYABLE_ERROR_PATTERNS.some((p) => msg.includes(p))) {
129
+ return true;
130
+ }
131
+ }
132
+ return false;
133
+ }
134
+ async function withRetry(fn, options) {
135
+ const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
136
+ let lastError;
137
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
138
+ try {
139
+ return await fn();
140
+ } catch (error) {
141
+ lastError = error;
142
+ if (attempt < opts.maxRetries && isRetryableError(error, opts)) {
143
+ const delay = backoffDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
144
+ debugLog("ollama", `Retry ${attempt + 1}/${opts.maxRetries} after ${delay}ms: ${error instanceof Error ? error.message : String(error)}`);
145
+ await new Promise((r) => setTimeout(r, delay));
146
+ continue;
147
+ }
148
+ throw error;
149
+ }
150
+ }
151
+ throw lastError;
152
+ }
153
+ async function fetchOllamaModels(baseUrl) {
154
+ return withRetry(async () => {
155
+ const res = await fetch(`${baseUrl}/api/tags`, {
156
+ signal: AbortSignal.timeout(5e3)
89
157
  });
90
- if (!res.ok) return void 0;
158
+ if (!res.ok) throw new Error(`Ollama returned ${res.status}`);
91
159
  const data = await res.json();
92
- for (const key of Object.keys(data?.model_info ?? {})) {
93
- if (key.endsWith(".context_length")) {
94
- const val = data.model_info[key];
95
- if (typeof val === "number") return val;
160
+ return data.models ?? [];
161
+ });
162
+ }
163
+ async function fetchModelContextLength(baseUrl, modelName) {
164
+ return withRetry(async () => {
165
+ try {
166
+ const res = await fetch(`${baseUrl}/api/show`, {
167
+ method: "POST",
168
+ headers: { "Content-Type": "application/json" },
169
+ body: JSON.stringify({ name: modelName }),
170
+ signal: AbortSignal.timeout(3e4)
171
+ });
172
+ if (!res.ok) return void 0;
173
+ const data = await res.json();
174
+ for (const key of Object.keys(data?.model_info ?? {})) {
175
+ if (key.endsWith(".context_length")) {
176
+ const val = data.model_info[key];
177
+ if (typeof val === "number") return val;
178
+ }
96
179
  }
180
+ const numCtx = data?.model_info?.["num_ctx"];
181
+ if (typeof numCtx === "number") return numCtx;
182
+ } catch {
183
+ return void 0;
97
184
  }
98
- const numCtx = data?.model_info?.["num_ctx"];
99
- if (typeof numCtx === "number") return numCtx;
100
- } catch {
101
185
  return void 0;
102
- }
186
+ });
103
187
  }
104
188
  async function fetchContextLengthsBatched(baseUrl, modelNames, batchSize = 3) {
105
189
  const result = /* @__PURE__ */ new Map();
@@ -205,6 +289,7 @@ export {
205
289
  BUILTIN_PROVIDERS,
206
290
  EXTENSION_VERSION,
207
291
  MODELS_JSON_PATH,
292
+ acquireModelsJsonLock,
208
293
  detectModelFamily,
209
294
  detectProvider,
210
295
  fetchContextLengthsBatched,
@@ -213,5 +298,7 @@ export {
213
298
  getOllamaBaseUrl,
214
299
  isReasoningModel,
215
300
  readModelsJson,
301
+ readModifyWriteModelsJson,
302
+ withRetry,
216
303
  writeModelsJson
217
304
  };
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@vtstech/pi-shared",
3
- "version": "1.1.5",
3
+ "version": "1.1.7",
4
4
  "description": "Shared utilities for Pi Coding Agent extensions",
5
5
  "exports": {
6
+ "./debug": "./debug.js",
6
7
  "./format": "./format.js",
8
+ "./model-test-utils": "./model-test-utils.js",
7
9
  "./ollama": "./ollama.js",
10
+ "./react-parser": "./react-parser.js",
8
11
  "./security": "./security.js",
9
12
  "./types": "./types.js"
10
13
  },
package/security.js CHANGED
@@ -12,6 +12,7 @@ function debugLog(module, message, ...args) {
12
12
  }
13
13
 
14
14
  // shared/security.ts
15
+ import dns from "node:dns";
15
16
  var SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
16
17
  var SECURITY_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "security.json");
17
18
  function getSecurityMode() {
@@ -248,6 +249,45 @@ function validatePath(filePath, allowedDirs) {
248
249
  }
249
250
  return { valid: false, error: `Path not in allowed directories: ${filePath}` };
250
251
  }
252
+ function isLoopbackIp(ip) {
253
+ if (ip.startsWith("127.") || ip === "0.0.0.0") return true;
254
+ if (ip === "::1" || ip === "::ffff:0.0.0.0") return true;
255
+ if (ip.startsWith("::ffff:127.")) return true;
256
+ return false;
257
+ }
258
+ function isPrivateIp(ip) {
259
+ if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true;
260
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
261
+ if (ip === "169.254.169.254") return true;
262
+ if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
263
+ if (ip.startsWith("fe80:")) return true;
264
+ return false;
265
+ }
266
+ async function resolveAndCheckHostname(hostname, blockPrivate = true) {
267
+ try {
268
+ const addresses = await new Promise((resolve2, reject) => {
269
+ dns.lookup(hostname, { all: true }, (err, addresses2) => {
270
+ if (err) reject(err);
271
+ else resolve2(addresses2);
272
+ });
273
+ });
274
+ if (!addresses || addresses.length === 0) {
275
+ return { safe: true, error: "" };
276
+ }
277
+ for (const addr of addresses) {
278
+ const ip = addr.address;
279
+ if (ip === "169.254.169.254") {
280
+ return { safe: false, error: `SSRF protection: hostname ${hostname} resolves to cloud metadata IP ${ip}` };
281
+ }
282
+ if (blockPrivate && (isLoopbackIp(ip) || isPrivateIp(ip))) {
283
+ return { safe: false, error: `SSRF protection: hostname ${hostname} resolves to private/reserved IP ${ip} (DNS rebinding check)` };
284
+ }
285
+ }
286
+ return { safe: true, error: "" };
287
+ } catch {
288
+ return { safe: true, error: "" };
289
+ }
290
+ }
251
291
  function isSafeUrl(url, blockSsrf = true) {
252
292
  if (!url) return { safe: false, error: "URL cannot be empty" };
253
293
  let parsed;
@@ -312,8 +352,13 @@ function sanitizeCommand(command) {
312
352
  let baseCmd = parts[0].toLowerCase();
313
353
  if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
314
354
  if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
315
- if (CRITICAL_COMMANDS.has(baseCmd)) {
316
- return { isSafe: false, error: `Blocked command: ${baseCmd} (critical)`, command: "" };
355
+ for (const raw of parts) {
356
+ let word = raw.toLowerCase();
357
+ if (word.includes("/")) word = word.split("/").pop();
358
+ if (word.includes("\\")) word = word.split("\\").pop();
359
+ if (CRITICAL_COMMANDS.has(word)) {
360
+ return { isSafe: false, error: `Blocked command: ${word} (critical)`, command: "" };
361
+ }
317
362
  }
318
363
  const mode = getSecurityMode();
319
364
  if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
@@ -332,16 +377,46 @@ function sanitizeCommand(command) {
332
377
  }
333
378
  var AUDIT_DIR = path.join(os.homedir(), ".pi", "agent");
334
379
  var AUDIT_LOG_PATH = path.join(AUDIT_DIR, "audit.log");
335
- function appendAuditEntry(entry) {
380
+ var AUDIT_BUFFER_MAX_ENTRIES = 50;
381
+ var AUDIT_FLUSH_INTERVAL_MS = 500;
382
+ var _auditBuffer = [];
383
+ var _auditFlushTimer = null;
384
+ function ensureAuditFlushTimer() {
385
+ if (_auditFlushTimer) return;
386
+ _auditFlushTimer = setInterval(() => {
387
+ if (_auditBuffer.length > 0) {
388
+ flushAuditBuffer();
389
+ }
390
+ }, AUDIT_FLUSH_INTERVAL_MS);
391
+ const timerRef = _auditFlushTimer;
392
+ if (timerRef.unref) {
393
+ timerRef.unref();
394
+ }
395
+ }
396
+ function flushAuditBuffer() {
397
+ if (_auditBuffer.length === 0) return;
336
398
  try {
337
399
  if (!fs.existsSync(AUDIT_DIR)) {
338
400
  fs.mkdirSync(AUDIT_DIR, { recursive: true });
339
401
  }
402
+ const batch = _auditBuffer.join("");
403
+ fs.appendFileSync(AUDIT_LOG_PATH, batch, "utf-8");
404
+ } catch (err) {
405
+ debugLog("security", "audit buffer flush failure", err);
406
+ }
407
+ _auditBuffer = [];
408
+ }
409
+ function appendAuditEntry(entry) {
410
+ try {
411
+ ensureAuditFlushTimer();
340
412
  const enriched = { ...entry, securityMode: getSecurityMode() };
341
413
  const line = JSON.stringify(enriched) + "\n";
342
- fs.appendFileSync(AUDIT_LOG_PATH, line, "utf-8");
414
+ _auditBuffer.push(line);
415
+ if (_auditBuffer.length >= AUDIT_BUFFER_MAX_ENTRIES) {
416
+ flushAuditBuffer();
417
+ }
343
418
  } catch (err) {
344
- debugLog("security", "audit log write failure", err);
419
+ debugLog("security", "audit log entry creation failure", err);
345
420
  }
346
421
  }
347
422
  function readRecentAuditEntries(count = 50) {
@@ -432,9 +507,11 @@ export {
432
507
  checkFileToolInput,
433
508
  checkHttpToolInput,
434
509
  checkInjectionPatterns,
510
+ flushAuditBuffer,
435
511
  getSecurityMode,
436
512
  isSafeUrl,
437
513
  readRecentAuditEntries,
514
+ resolveAndCheckHostname,
438
515
  sanitizeCommand,
439
516
  setSecurityMode,
440
517
  validatePath