@vtstech/pi-shared 1.1.7 → 1.1.9

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/config-io.js ADDED
@@ -0,0 +1,57 @@
1
+ // shared/config-io.ts
2
+ import * as fs from "node:fs";
3
+ import * as path from "node:path";
4
+ import os from "node:os";
5
+
6
+ // shared/debug.ts
7
+ var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
8
+ function debugLog(module, message, ...args) {
9
+ if (!DEBUG_ENABLED) return;
10
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
11
+ console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
12
+ }
13
+
14
+ // shared/config-io.ts
15
+ var PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
16
+ function readJsonConfig(filePath, defaultValue = {}) {
17
+ try {
18
+ if (fs.existsSync(filePath)) {
19
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
20
+ }
21
+ } catch (err) {
22
+ debugLog("config-io", `failed to read config: ${filePath}`, err);
23
+ }
24
+ return defaultValue;
25
+ }
26
+ function writeJsonConfig(filePath, data) {
27
+ const dir = path.dirname(filePath);
28
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
29
+ const content = JSON.stringify(data, null, 2) + "\n";
30
+ const tmpPath = filePath + ".tmp";
31
+ try {
32
+ fs.writeFileSync(tmpPath, content, "utf-8");
33
+ fs.renameSync(tmpPath, filePath);
34
+ } catch {
35
+ fs.writeFileSync(filePath, content, "utf-8");
36
+ }
37
+ }
38
+ var SETTINGS_PATH = path.join(PI_AGENT_DIR, "settings.json");
39
+ var SECURITY_PATH = path.join(PI_AGENT_DIR, "security.json");
40
+ var REACT_MODE_PATH = path.join(PI_AGENT_DIR, "react-mode.json");
41
+ var MODEL_TEST_CONFIG_PATH = path.join(PI_AGENT_DIR, "model-test-config.json");
42
+ function readSettings() {
43
+ return readJsonConfig(SETTINGS_PATH);
44
+ }
45
+ function writeSettings(data) {
46
+ writeJsonConfig(SETTINGS_PATH, data);
47
+ }
48
+ export {
49
+ MODEL_TEST_CONFIG_PATH,
50
+ REACT_MODE_PATH,
51
+ SECURITY_PATH,
52
+ SETTINGS_PATH,
53
+ readJsonConfig,
54
+ readSettings,
55
+ writeJsonConfig,
56
+ writeSettings
57
+ };
package/errors.js ADDED
@@ -0,0 +1,56 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
+ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
+
5
+ // shared/errors.ts
6
+ var ExtensionError = class extends Error {
7
+ constructor(message, code) {
8
+ super(message);
9
+ __publicField(this, "code", code);
10
+ this.name = "ExtensionError";
11
+ }
12
+ };
13
+ var ConfigError = class extends ExtensionError {
14
+ constructor(message) {
15
+ super(message, "CONFIG_ERROR");
16
+ this.name = "ConfigError";
17
+ }
18
+ };
19
+ var ApiError = class extends ExtensionError {
20
+ constructor(message, statusCode, url) {
21
+ super(message, "API_ERROR");
22
+ __publicField(this, "statusCode", statusCode);
23
+ __publicField(this, "url", url);
24
+ this.name = "ApiError";
25
+ }
26
+ };
27
+ var ExtensionTimeoutError = class extends ExtensionError {
28
+ constructor(message, timeoutMs) {
29
+ super(message, "TIMEOUT");
30
+ __publicField(this, "timeoutMs", timeoutMs);
31
+ this.name = "ExtensionTimeoutError";
32
+ }
33
+ };
34
+ var SecurityError = class extends ExtensionError {
35
+ constructor(message, rule, detail) {
36
+ super(message, "SECURITY_VIOLATION");
37
+ __publicField(this, "rule", rule);
38
+ __publicField(this, "detail", detail);
39
+ this.name = "SecurityError";
40
+ }
41
+ };
42
+ var ToolError = class extends ExtensionError {
43
+ constructor(message, toolName) {
44
+ super(message, "TOOL_ERROR");
45
+ __publicField(this, "toolName", toolName);
46
+ this.name = "ToolError";
47
+ }
48
+ };
49
+ export {
50
+ ApiError,
51
+ ConfigError,
52
+ ExtensionError,
53
+ ExtensionTimeoutError,
54
+ SecurityError,
55
+ ToolError
56
+ };
package/format.js CHANGED
@@ -32,6 +32,7 @@ function msHuman(ms) {
32
32
  }
33
33
  function fmtBytes(b) {
34
34
  if (b === 0) return "0B";
35
+ if (b < 1024) return `${b}B`;
35
36
  if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)}G`;
36
37
  if (b >= 1048576) return `${(b / 1048576).toFixed(0)}M`;
37
38
  return `${(b / 1024).toFixed(0)}K`;
@@ -34,6 +34,9 @@ var CONFIG = {
34
34
  // Effectively unlimited for cloud provider API calls
35
35
  PROVIDER_TOOL_TIMEOUT_MS: 12e4,
36
36
  // 120 seconds for tool usage tests on providers
37
+ // Context length fetching
38
+ CONTEXT_BATCH_SIZE: 3,
39
+ // Concurrent requests when fetching model context lengths
37
40
  // Rate limiting
38
41
  TEST_DELAY_MS: 1e4
39
42
  // 10 seconds between tests to avoid rate limiting
@@ -62,6 +65,7 @@ function getEffectiveConfig() {
62
65
  TOOL_TEST_TIMEOUT_MS: userConfig.toolTestTimeoutMs ?? CONFIG.TOOL_TEST_TIMEOUT_MS,
63
66
  PROVIDER_TIMEOUT_MS: userConfig.providerTimeoutMs ?? CONFIG.PROVIDER_TIMEOUT_MS,
64
67
  PROVIDER_TOOL_TIMEOUT_MS: userConfig.providerToolTimeoutMs ?? CONFIG.PROVIDER_TOOL_TIMEOUT_MS,
68
+ CONTEXT_BATCH_SIZE: userConfig.contextBatchSize ?? CONFIG.CONTEXT_BATCH_SIZE,
65
69
  NUM_PREDICT: userConfig.numPredict ?? CONFIG.NUM_PREDICT,
66
70
  TEMPERATURE: userConfig.temperature ?? CONFIG.TEMPERATURE
67
71
  };
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.7";
15
+ var EXTENSION_VERSION = "1.1.9";
16
16
  var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
17
17
  var _modelsJsonCache = null;
18
18
  var _ollamaBaseUrlCache = null;
@@ -179,7 +179,8 @@ async function fetchModelContextLength(baseUrl, modelName) {
179
179
  }
180
180
  const numCtx = data?.model_info?.["num_ctx"];
181
181
  if (typeof numCtx === "number") return numCtx;
182
- } catch {
182
+ } catch (err) {
183
+ debugLog("ollama", `failed to fetch context length for ${modelName}`, err);
183
184
  return void 0;
184
185
  }
185
186
  return void 0;
@@ -213,7 +214,8 @@ var BUILTIN_PROVIDERS = {
213
214
  xai: { api: "openai-completions", baseUrl: "https://api.x.ai/v1", envKey: "XAI_API_KEY" },
214
215
  together: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1", envKey: "TOGETHER_API_KEY" },
215
216
  fireworks: { api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", envKey: "FIREWORKS_API_KEY" },
216
- cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com/v1", envKey: "COHERE_API_KEY" }
217
+ cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com/v1", envKey: "COHERE_API_KEY" },
218
+ zai: { api: "openai-completions", baseUrl: "https://open.bigmodel.cn/api/paas/v4", envKey: "ZAI_API_KEY" }
217
219
  };
218
220
  function detectModelFamily(modelName) {
219
221
  const name = modelName.toLowerCase();
@@ -233,6 +235,8 @@ function detectModelFamily(modelName) {
233
235
  ["gemma", "gemma2"],
234
236
  ["granite", "granite"],
235
237
  ["dolphin", "dolphin"],
238
+ ["glm-4", "glm"],
239
+ ["glm", "glm"],
236
240
  ["deepseek-r1", "deepseek-r1"],
237
241
  ["deepseek", "deepseek"],
238
242
  ["mistral", "qwen2"],
@@ -285,6 +289,11 @@ function detectProvider(ctx) {
285
289
  }
286
290
  return { kind: "unknown", name: providerName };
287
291
  }
292
+ function isLocalProvider(baseUrl, providerName) {
293
+ if (providerName === "ollama") return true;
294
+ const url = baseUrl || "";
295
+ return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0");
296
+ }
288
297
  export {
289
298
  BUILTIN_PROVIDERS,
290
299
  EXTENSION_VERSION,
@@ -296,6 +305,7 @@ export {
296
305
  fetchModelContextLength,
297
306
  fetchOllamaModels,
298
307
  getOllamaBaseUrl,
308
+ isLocalProvider,
299
309
  isReasoningModel,
300
310
  readModelsJson,
301
311
  readModifyWriteModelsJson,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vtstech/pi-shared",
3
- "version": "1.1.7",
3
+ "version": "1.1.9",
4
4
  "description": "Shared utilities for Pi Coding Agent extensions",
5
5
  "exports": {
6
6
  "./debug": "./debug.js",
@@ -0,0 +1,18 @@
1
+ // shared/provider-sync.ts
2
+ function mergeModels(newModels, oldModels) {
3
+ const oldModelMap = new Map(oldModels.map((m) => [m.id, m]));
4
+ return newModels.map((m) => {
5
+ const old = oldModelMap.get(m.id);
6
+ if (old) {
7
+ const merged = { ...m };
8
+ for (const [k, v] of Object.entries(old)) {
9
+ if (!(k in m)) merged[k] = v;
10
+ }
11
+ return merged;
12
+ }
13
+ return m;
14
+ });
15
+ }
16
+ export {
17
+ mergeModels
18
+ };
package/react-parser.js CHANGED
@@ -111,6 +111,23 @@ function extractJsonArgs(rawArgs) {
111
111
  if (cmdMatch) return { command: cmdMatch[1] };
112
112
  return { input: jsonStr };
113
113
  }
114
+ function extractBraceJson(raw) {
115
+ const jsonStart = raw.indexOf("{");
116
+ if (jsonStart === -1) return "";
117
+ let depth = 0;
118
+ let jsonEnd = -1;
119
+ for (let i = jsonStart; i < raw.length; i++) {
120
+ if (raw[i] === "{") depth++;
121
+ else if (raw[i] === "}") {
122
+ depth--;
123
+ if (depth === 0) {
124
+ jsonEnd = i;
125
+ break;
126
+ }
127
+ }
128
+ }
129
+ return jsonEnd !== -1 ? raw.slice(jsonStart, jsonEnd + 1) : "";
130
+ }
114
131
  function parseReact(text) {
115
132
  for (const dp of ALL_DIALECT_PATTERNS) {
116
133
  const result = parseReactWithPatterns(text, dp);
@@ -382,6 +399,7 @@ export {
382
399
  WORD_MAPPINGS,
383
400
  buildDialectPatterns,
384
401
  detectReactDialect,
402
+ extractBraceJson,
385
403
  extractJsonArgs,
386
404
  extractToolFromJson,
387
405
  fuzzyMatchToolName,
package/security.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // shared/security.ts
2
2
  import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import os from "node:os";
3
+ import * as path2 from "node:path";
4
+ import os2 from "node:os";
5
5
 
6
6
  // shared/debug.ts
7
7
  var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
@@ -13,8 +13,19 @@ function debugLog(module, message, ...args) {
13
13
 
14
14
  // shared/security.ts
15
15
  import dns from "node:dns";
16
- var SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
17
- var SECURITY_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "security.json");
16
+
17
+ // shared/config-io.ts
18
+ import * as path from "node:path";
19
+ import os from "node:os";
20
+ var PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
21
+ var SETTINGS_PATH = path.join(PI_AGENT_DIR, "settings.json");
22
+ var SECURITY_PATH = path.join(PI_AGENT_DIR, "security.json");
23
+ var REACT_MODE_PATH = path.join(PI_AGENT_DIR, "react-mode.json");
24
+ var MODEL_TEST_CONFIG_PATH = path.join(PI_AGENT_DIR, "model-test-config.json");
25
+
26
+ // shared/security.ts
27
+ var SETTINGS_PATH2 = SETTINGS_PATH;
28
+ var SECURITY_CONFIG_PATH = SECURITY_PATH;
18
29
  function getSecurityMode() {
19
30
  try {
20
31
  if (!fs.existsSync(SECURITY_CONFIG_PATH)) return "max";
@@ -28,7 +39,7 @@ function getSecurityMode() {
28
39
  }
29
40
  }
30
41
  function setSecurityMode(mode) {
31
- const configDir = path.dirname(SECURITY_CONFIG_PATH);
42
+ const configDir = path2.dirname(SECURITY_CONFIG_PATH);
32
43
  try {
33
44
  if (!fs.existsSync(configDir)) {
34
45
  fs.mkdirSync(configDir, { recursive: true });
@@ -159,6 +170,8 @@ var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
159
170
  "172.29.",
160
171
  "172.30.",
161
172
  "172.31.",
173
+ // IPv6-mapped IPv4 cloud metadata (always blocked)
174
+ "::ffff:169.254.169.254",
162
175
  // Internal service patterns
163
176
  "internal.",
164
177
  "private.",
@@ -172,6 +185,25 @@ var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
172
185
  "::1",
173
186
  "::ffff:127.0.0.1",
174
187
  "::ffff:0.0.0.0",
188
+ // IPv6-mapped IPv4 private ranges (always blocked in max mode)
189
+ "::ffff:10.",
190
+ "::ffff:192.168.",
191
+ "::ffff:172.16.",
192
+ "::ffff:172.17.",
193
+ "::ffff:172.18.",
194
+ "::ffff:172.19.",
195
+ "::ffff:172.20.",
196
+ "::ffff:172.21.",
197
+ "::ffff:172.22.",
198
+ "::ffff:172.23.",
199
+ "::ffff:172.24.",
200
+ "::ffff:172.25.",
201
+ "::ffff:172.26.",
202
+ "::ffff:172.27.",
203
+ "::ffff:172.28.",
204
+ "::ffff:172.29.",
205
+ "::ffff:172.30.",
206
+ "::ffff:172.31.",
175
207
  // Local/internal patterns
176
208
  "local."
177
209
  ]);
@@ -201,7 +233,7 @@ function validatePath(filePath, allowedDirs) {
201
233
  }
202
234
  let resolved;
203
235
  try {
204
- resolved = path.resolve(filePath);
236
+ resolved = path2.resolve(filePath);
205
237
  try {
206
238
  resolved = fs.realpathSync(resolved);
207
239
  } catch {
@@ -219,9 +251,9 @@ function validatePath(filePath, allowedDirs) {
219
251
  "/etc/passwd",
220
252
  "/.ssh/",
221
253
  "/.gnupg/",
222
- path.join(os.homedir(), ".ssh"),
223
- path.join(os.homedir(), ".gnupg"),
224
- SETTINGS_PATH,
254
+ path2.join(os2.homedir(), ".ssh"),
255
+ path2.join(os2.homedir(), ".gnupg"),
256
+ SETTINGS_PATH2,
225
257
  SECURITY_CONFIG_PATH
226
258
  // NOTE: models.json is intentionally excluded from sensitivePaths.
227
259
  // Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
@@ -234,14 +266,14 @@ function validatePath(filePath, allowedDirs) {
234
266
  }
235
267
  }
236
268
  const cwd = process.cwd();
237
- const safePrefixes = ["/tmp", "/var/tmp", "/home", cwd];
269
+ const safePrefixes = ["/home", "/tmp", cwd];
238
270
  for (const prefix of safePrefixes) {
239
- if (resolved.startsWith(prefix)) return { valid: true, error: "" };
271
+ if (resolved.startsWith(prefix + "/") || resolved === prefix) return { valid: true, error: "" };
240
272
  }
241
273
  if (allowedDirs) {
242
274
  for (const dir of allowedDirs) {
243
275
  try {
244
- const absDir = path.resolve(dir);
276
+ const absDir = path2.resolve(dir);
245
277
  if (resolved.startsWith(absDir)) return { valid: true, error: "" };
246
278
  } catch {
247
279
  }
@@ -249,16 +281,23 @@ function validatePath(filePath, allowedDirs) {
249
281
  }
250
282
  return { valid: false, error: `Path not in allowed directories: ${filePath}` };
251
283
  }
284
+ function stripIpv6Mapped(ip) {
285
+ if (ip.startsWith("::ffff:") && !ip.startsWith("::ffff:0:0")) {
286
+ return ip.slice(7);
287
+ }
288
+ return ip;
289
+ }
252
290
  function isLoopbackIp(ip) {
253
- if (ip.startsWith("127.") || ip === "0.0.0.0") return true;
291
+ const norm = stripIpv6Mapped(ip);
292
+ if (norm.startsWith("127.") || norm === "0.0.0.0") return true;
254
293
  if (ip === "::1" || ip === "::ffff:0.0.0.0") return true;
255
- if (ip.startsWith("::ffff:127.")) return true;
256
294
  return false;
257
295
  }
258
296
  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;
297
+ const norm = stripIpv6Mapped(ip);
298
+ if (norm.startsWith("10.") || norm.startsWith("192.168.")) return true;
299
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(norm)) return true;
300
+ if (norm === "169.254.169.254") return true;
262
301
  if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
263
302
  if (ip.startsWith("fe80:")) return true;
264
303
  return false;
@@ -276,7 +315,8 @@ async function resolveAndCheckHostname(hostname, blockPrivate = true) {
276
315
  }
277
316
  for (const addr of addresses) {
278
317
  const ip = addr.address;
279
- if (ip === "169.254.169.254") {
318
+ const normIp = stripIpv6Mapped(ip);
319
+ if (normIp === "169.254.169.254") {
280
320
  return { safe: false, error: `SSRF protection: hostname ${hostname} resolves to cloud metadata IP ${ip}` };
281
321
  }
282
322
  if (blockPrivate && (isLoopbackIp(ip) || isPrivateIp(ip))) {
@@ -316,12 +356,24 @@ function isSafeUrl(url, blockSsrf = true) {
316
356
  const mode = getSecurityMode();
317
357
  for (const pattern of BLOCKED_URL_ALWAYS) {
318
358
  if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
359
+ if (/^\d|^::/.test(pattern)) {
360
+ const nextChar = normalized[pattern.length];
361
+ if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
362
+ continue;
363
+ }
364
+ }
319
365
  return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
320
366
  }
321
367
  }
322
368
  if (mode === "max") {
323
369
  for (const pattern of BLOCKED_URL_MAX_ONLY) {
324
370
  if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
371
+ if (/^\d|^::/.test(pattern)) {
372
+ const nextChar = normalized[pattern.length];
373
+ if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
374
+ continue;
375
+ }
376
+ }
325
377
  return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}' (max mode)` };
326
378
  }
327
379
  }
@@ -330,25 +382,20 @@ function isSafeUrl(url, blockSsrf = true) {
330
382
  return { safe: true, error: "" };
331
383
  }
332
384
  var INJECTION_PATTERNS = [
333
- // Command chaining with dangerous commands only
385
+ // Semicolon chaining to dangerous commands — mode-independent.
386
+ // Unlike && (conditional), ; ALWAYS runs the second command.
334
387
  /;\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill|pkill)\b/i,
335
- // Piping to dangerous commands
336
- /\|\s*(rm|sudo|chmod|chown|shred|mkfs)\b/i,
337
- // AND chaining with dangerous commands
338
- /&&\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill)\b/i,
339
388
  // Command substitution (backticks) — still dangerous
340
389
  /`[^`]+`/,
341
390
  // Command substitution ($()) — still dangerous
342
391
  /\$\([^)]+\)/,
343
392
  // Variable expansion targeting sensitive env vars
344
- /\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i,
345
- // Bare pipe without space (likely injection, not intentional piping)
346
- /\|(?=[^\s|])/
393
+ /\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
347
394
  ];
348
- function sanitizeCommand(command) {
349
- if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
350
- const parts = command.trim().split(/\s+/);
351
- if (!parts.length) return { isSafe: false, error: "Command cannot be empty", command: "" };
395
+ function checkSingleCommand(command, mode) {
396
+ const trimmed = command.trim();
397
+ if (!trimmed) return { isSafe: true, error: "", command: "" };
398
+ const parts = trimmed.split(/\s+/);
352
399
  let baseCmd = parts[0].toLowerCase();
353
400
  if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
354
401
  if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
@@ -360,23 +407,57 @@ function sanitizeCommand(command) {
360
407
  return { isSafe: false, error: `Blocked command: ${word} (critical)`, command: "" };
361
408
  }
362
409
  }
363
- const mode = getSecurityMode();
364
410
  if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
365
411
  return { isSafe: false, error: `Blocked command: ${baseCmd} (max mode)`, command: "" };
366
412
  }
367
- const stripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
368
- if (stripped !== command) {
413
+ for (const pattern of INJECTION_PATTERNS) {
414
+ if (pattern.test(trimmed)) {
415
+ return { isSafe: false, error: `Potential injection pattern detected in: ${trimmed}`, command: "" };
416
+ }
417
+ }
418
+ return { isSafe: true, error: "", command: trimmed };
419
+ }
420
+ function sanitizeCommand(command) {
421
+ if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
422
+ let normalizedCmd = command.normalize("NFKC");
423
+ normalizedCmd = normalizedCmd.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "");
424
+ const strippedForCompare = command.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "").normalize("NFKC");
425
+ if (normalizedCmd !== strippedForCompare) {
426
+ return { isSafe: false, error: `Command rejected: Unicode normalization variance detected (possible homoglyph bypass)`, command: "" };
427
+ }
428
+ command = normalizedCmd;
429
+ const trimmed = command.trim();
430
+ if (!trimmed) return { isSafe: false, error: "Command cannot be empty", command: "" };
431
+ const newlineStripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
432
+ if (newlineStripped !== command) {
369
433
  return { isSafe: false, error: "Newline characters detected: potential command injection", command: "" };
370
434
  }
371
435
  for (const pattern of INJECTION_PATTERNS) {
372
- if (pattern.test(command)) {
436
+ if (pattern.test(trimmed)) {
373
437
  return { isSafe: false, error: `Potential injection pattern detected`, command: "" };
374
438
  }
375
439
  }
440
+ const subCommands = [];
441
+ let remaining = trimmed;
442
+ const chainRegex = /&&|\|\||(?<!\|)\|(?!\|)/g;
443
+ let match;
444
+ let lastIndex = 0;
445
+ while ((match = chainRegex.exec(remaining)) !== null) {
446
+ subCommands.push(remaining.slice(lastIndex, match.index));
447
+ lastIndex = match.index + match[0].length;
448
+ }
449
+ subCommands.push(remaining.slice(lastIndex));
450
+ const mode = getSecurityMode();
451
+ for (const subCmd of subCommands) {
452
+ const result = checkSingleCommand(subCmd, mode);
453
+ if (!result.isSafe) {
454
+ return { isSafe: false, error: result.error, command: "" };
455
+ }
456
+ }
376
457
  return { isSafe: true, error: "", command };
377
458
  }
378
- var AUDIT_DIR = path.join(os.homedir(), ".pi", "agent");
379
- var AUDIT_LOG_PATH = path.join(AUDIT_DIR, "audit.log");
459
+ var AUDIT_DIR = path2.join(os2.homedir(), ".pi", "agent");
460
+ var AUDIT_LOG_PATH = path2.join(AUDIT_DIR, "audit.log");
380
461
  var AUDIT_BUFFER_MAX_ENTRIES = 50;
381
462
  var AUDIT_FLUSH_INTERVAL_MS = 500;
382
463
  var _auditBuffer = [];
@@ -409,6 +490,19 @@ function flushAuditBuffer() {
409
490
  function appendAuditEntry(entry) {
410
491
  try {
411
492
  ensureAuditFlushTimer();
493
+ const AUDIT_LOG_MAX_SIZE = 5 * 1024 * 1024;
494
+ try {
495
+ if (fs.existsSync(AUDIT_LOG_PATH)) {
496
+ const stat = fs.statSync(AUDIT_LOG_PATH);
497
+ if (stat.size > AUDIT_LOG_MAX_SIZE) {
498
+ const entries = readRecentAuditEntries(1e3);
499
+ const content = entries.map((e) => JSON.stringify(e)).join("\n") + "\n";
500
+ fs.writeFileSync(AUDIT_LOG_PATH, content, "utf-8");
501
+ }
502
+ }
503
+ } catch (err) {
504
+ debugLog("security", "audit log rotation failed", err);
505
+ }
412
506
  const enriched = { ...entry, securityMode: getSecurityMode() };
413
507
  const line = JSON.stringify(enriched) + "\n";
414
508
  _auditBuffer.push(line);
@@ -422,8 +516,31 @@ function appendAuditEntry(entry) {
422
516
  function readRecentAuditEntries(count = 50) {
423
517
  try {
424
518
  if (!fs.existsSync(AUDIT_LOG_PATH)) return [];
425
- const content = fs.readFileSync(AUDIT_LOG_PATH, "utf-8");
426
- const lines = content.trim().split("\n");
519
+ const fileSize = fs.statSync(AUDIT_LOG_PATH).size;
520
+ if (fileSize === 0) return [];
521
+ const fd = fs.openSync(AUDIT_LOG_PATH, "r");
522
+ const bufferSize = 8192;
523
+ const buffer = Buffer.alloc(bufferSize);
524
+ const lines = [];
525
+ let pos = fileSize;
526
+ let partial = "";
527
+ while (pos > 0 && lines.length < count) {
528
+ const readSize = Math.min(bufferSize, pos);
529
+ pos -= readSize;
530
+ fs.readSync(fd, buffer, 0, readSize, pos);
531
+ const chunk = buffer.slice(0, readSize).toString("utf-8");
532
+ partial = chunk + partial;
533
+ const lineBreak = partial.lastIndexOf("\n");
534
+ if (lineBreak !== -1) {
535
+ const complete = partial.slice(lineBreak + 1);
536
+ if (complete.trim()) lines.unshift(complete);
537
+ partial = partial.slice(0, lineBreak);
538
+ }
539
+ }
540
+ fs.closeSync(fd);
541
+ if (partial.trim() && lines.length < count) {
542
+ lines.unshift(partial);
543
+ }
427
544
  const recent = lines.slice(-count);
428
545
  return recent.map((line) => {
429
546
  try {
@@ -437,6 +554,12 @@ function readRecentAuditEntries(count = 50) {
437
554
  return [];
438
555
  }
439
556
  }
557
+ process.on("exit", () => {
558
+ flushAuditBuffer();
559
+ });
560
+ process.on("SIGTERM", () => {
561
+ flushAuditBuffer();
562
+ });
440
563
  function checkBashToolInput(input) {
441
564
  const command = input.command ?? input.cmd ?? "";
442
565
  if (!command) return { safe: true, rule: "", detail: "" };
@@ -501,7 +624,7 @@ export {
501
624
  CRITICAL_COMMANDS,
502
625
  EXTENDED_COMMANDS,
503
626
  SECURITY_CONFIG_PATH,
504
- SETTINGS_PATH,
627
+ SETTINGS_PATH2 as SETTINGS_PATH,
505
628
  appendAuditEntry,
506
629
  checkBashToolInput,
507
630
  checkFileToolInput,
package/test-report.js ADDED
@@ -0,0 +1,89 @@
1
+ // shared/format.ts
2
+ function section(title) {
3
+ return `
4
+ \u2500\u2500 ${title} ${"\u2500".repeat(Math.max(1, 60 - title.length - 4))}`;
5
+ }
6
+ function ok(msg) {
7
+ return ` \u2705 ${msg}`;
8
+ }
9
+ function fail(msg) {
10
+ return ` \u274C ${msg}`;
11
+ }
12
+ function warn(msg) {
13
+ return ` \u26A0\uFE0F ${msg}`;
14
+ }
15
+ function info(msg) {
16
+ return ` \u2139\uFE0F ${msg}`;
17
+ }
18
+ function msHuman(ms) {
19
+ if (ms < 1e3) return `${ms.toFixed(0)}ms`;
20
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
21
+ return `${(ms / 6e4).toFixed(1)}m`;
22
+ }
23
+
24
+ // shared/ollama.ts
25
+ import * as path from "node:path";
26
+ import os from "node:os";
27
+
28
+ // shared/debug.ts
29
+ var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
30
+
31
+ // shared/ollama.ts
32
+ var EXTENSION_VERSION = "1.1.9";
33
+ var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
34
+
35
+ // shared/test-report.ts
36
+ var branding = [
37
+ ` \u26A1 Pi Model Benchmark v${EXTENSION_VERSION}`,
38
+ ` Written by VTSTech`,
39
+ ` GitHub: https://github.com/VTSTech`,
40
+ ` Website: www.vts-tech.org`
41
+ ].join("\n");
42
+ function formatTestScore(score, label) {
43
+ switch (score) {
44
+ case "STRONG":
45
+ return ok(`${label} (${score})`);
46
+ case "MODERATE":
47
+ return ok(`${label} (${score})`);
48
+ case "WEAK":
49
+ return warn(`${label} (${score})`);
50
+ case "FAIL":
51
+ return fail(`${label} (${score})`);
52
+ case "ERROR":
53
+ return fail(`Error: ${label}`);
54
+ default:
55
+ return fail(`${label} (${score})`);
56
+ }
57
+ }
58
+ function formatTestSummary(tests, totalMs) {
59
+ const lines = [];
60
+ lines.push(section("SUMMARY"));
61
+ for (const t of tests) {
62
+ lines.push(t.pass ? ok(`${t.name}: ${t.score}`) : fail(`${t.name}: ${t.score}`));
63
+ }
64
+ lines.push(info(`Total time: ${msHuman(totalMs)}`));
65
+ const passed = tests.filter((t) => t.pass).length;
66
+ lines.push(info(`Score: ${passed}/${tests.length} tests passed`));
67
+ return lines;
68
+ }
69
+ function formatRecommendation(model, passed, total, via) {
70
+ const suffix = via ? ` via ${via}` : "";
71
+ const lines = [];
72
+ lines.push(section("RECOMMENDATION"));
73
+ if (passed === total) {
74
+ lines.push(ok(`${model} is a STRONG model${suffix} \u2014 full capability`));
75
+ } else if (passed > 0 && passed >= total - 1) {
76
+ lines.push(ok(`${model} is a GOOD model${suffix} \u2014 most capabilities work`));
77
+ } else if (passed > 0 && passed >= total - 2) {
78
+ lines.push(warn(`${model} is USABLE${suffix} \u2014 some capabilities are limited`));
79
+ } else {
80
+ lines.push(fail(`${model} is WEAK${suffix} \u2014 limited capabilities for agent use`));
81
+ }
82
+ return lines;
83
+ }
84
+ export {
85
+ branding,
86
+ formatRecommendation,
87
+ formatTestScore,
88
+ formatTestSummary
89
+ };