@vtstech/pi-shared 1.1.7 → 1.1.8

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,49 @@
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
+ var PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
6
+ function readJsonConfig(filePath, defaultValue = {}) {
7
+ try {
8
+ if (fs.existsSync(filePath)) {
9
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
10
+ }
11
+ } catch (err) {
12
+ if (typeof process !== "undefined" && process.env.PI_EXTENSIONS_DEBUG === "1") {
13
+ console.debug(`[config-io] Failed to read config: ${filePath}`, err);
14
+ }
15
+ }
16
+ return defaultValue;
17
+ }
18
+ function writeJsonConfig(filePath, data) {
19
+ const dir = path.dirname(filePath);
20
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
21
+ const content = JSON.stringify(data, null, 2) + "\n";
22
+ const tmpPath = filePath + ".tmp";
23
+ try {
24
+ fs.writeFileSync(tmpPath, content, "utf-8");
25
+ fs.renameSync(tmpPath, filePath);
26
+ } catch {
27
+ fs.writeFileSync(filePath, content, "utf-8");
28
+ }
29
+ }
30
+ var SETTINGS_PATH = path.join(PI_AGENT_DIR, "settings.json");
31
+ var SECURITY_PATH = path.join(PI_AGENT_DIR, "security.json");
32
+ var REACT_MODE_PATH = path.join(PI_AGENT_DIR, "react-mode.json");
33
+ var MODEL_TEST_CONFIG_PATH = path.join(PI_AGENT_DIR, "model-test-config.json");
34
+ function readSettings() {
35
+ return readJsonConfig(SETTINGS_PATH);
36
+ }
37
+ function writeSettings(data) {
38
+ writeJsonConfig(SETTINGS_PATH, data);
39
+ }
40
+ export {
41
+ MODEL_TEST_CONFIG_PATH,
42
+ REACT_MODE_PATH,
43
+ SECURITY_PATH,
44
+ SETTINGS_PATH,
45
+ readJsonConfig,
46
+ readSettings,
47
+ writeJsonConfig,
48
+ writeSettings
49
+ };
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 TimeoutError = class extends ExtensionError {
28
+ constructor(message, timeoutMs) {
29
+ super(message, "TIMEOUT");
30
+ __publicField(this, "timeoutMs", timeoutMs);
31
+ this.name = "TimeoutError";
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
+ SecurityError,
54
+ TimeoutError,
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.8";
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 ${model}`, err);
183
184
  return void 0;
184
185
  }
185
186
  return void 0;
@@ -246,9 +247,9 @@ function detectModelFamily(modelName) {
246
247
  return "unknown";
247
248
  }
248
249
  function detectProvider(ctx) {
249
- const model = ctx.model;
250
- if (!model) return { kind: "unknown", name: "none" };
251
- const providerName = model.provider || "";
250
+ const model2 = ctx.model;
251
+ if (!model2) return { kind: "unknown", name: "none" };
252
+ const providerName = model2.provider || "";
252
253
  if (!providerName) return { kind: "unknown", name: "none" };
253
254
  const modelsJson = readModelsJson();
254
255
  const userProviderCfg = (modelsJson.providers || {})[providerName];
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.8",
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
@@ -159,6 +159,8 @@ var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
159
159
  "172.29.",
160
160
  "172.30.",
161
161
  "172.31.",
162
+ // IPv6-mapped IPv4 cloud metadata (always blocked)
163
+ "::ffff:169.254.169.254",
162
164
  // Internal service patterns
163
165
  "internal.",
164
166
  "private.",
@@ -172,6 +174,25 @@ var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
172
174
  "::1",
173
175
  "::ffff:127.0.0.1",
174
176
  "::ffff:0.0.0.0",
177
+ // IPv6-mapped IPv4 private ranges (always blocked in max mode)
178
+ "::ffff:10.",
179
+ "::ffff:192.168.",
180
+ "::ffff:172.16.",
181
+ "::ffff:172.17.",
182
+ "::ffff:172.18.",
183
+ "::ffff:172.19.",
184
+ "::ffff:172.20.",
185
+ "::ffff:172.21.",
186
+ "::ffff:172.22.",
187
+ "::ffff:172.23.",
188
+ "::ffff:172.24.",
189
+ "::ffff:172.25.",
190
+ "::ffff:172.26.",
191
+ "::ffff:172.27.",
192
+ "::ffff:172.28.",
193
+ "::ffff:172.29.",
194
+ "::ffff:172.30.",
195
+ "::ffff:172.31.",
175
196
  // Local/internal patterns
176
197
  "local."
177
198
  ]);
@@ -234,9 +255,9 @@ function validatePath(filePath, allowedDirs) {
234
255
  }
235
256
  }
236
257
  const cwd = process.cwd();
237
- const safePrefixes = ["/tmp", "/var/tmp", "/home", cwd];
258
+ const safePrefixes = ["/home", "/tmp", cwd];
238
259
  for (const prefix of safePrefixes) {
239
- if (resolved.startsWith(prefix)) return { valid: true, error: "" };
260
+ if (resolved.startsWith(prefix + "/") || resolved === prefix) return { valid: true, error: "" };
240
261
  }
241
262
  if (allowedDirs) {
242
263
  for (const dir of allowedDirs) {
@@ -249,16 +270,23 @@ function validatePath(filePath, allowedDirs) {
249
270
  }
250
271
  return { valid: false, error: `Path not in allowed directories: ${filePath}` };
251
272
  }
273
+ function stripIpv6Mapped(ip) {
274
+ if (ip.startsWith("::ffff:") && !ip.startsWith("::ffff:0:0")) {
275
+ return ip.slice(7);
276
+ }
277
+ return ip;
278
+ }
252
279
  function isLoopbackIp(ip) {
253
- if (ip.startsWith("127.") || ip === "0.0.0.0") return true;
280
+ const norm = stripIpv6Mapped(ip);
281
+ if (norm.startsWith("127.") || norm === "0.0.0.0") return true;
254
282
  if (ip === "::1" || ip === "::ffff:0.0.0.0") return true;
255
- if (ip.startsWith("::ffff:127.")) return true;
256
283
  return false;
257
284
  }
258
285
  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;
286
+ const norm = stripIpv6Mapped(ip);
287
+ if (norm.startsWith("10.") || norm.startsWith("192.168.")) return true;
288
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(norm)) return true;
289
+ if (norm === "169.254.169.254") return true;
262
290
  if (ip.startsWith("fc") || ip.startsWith("fd")) return true;
263
291
  if (ip.startsWith("fe80:")) return true;
264
292
  return false;
@@ -276,7 +304,8 @@ async function resolveAndCheckHostname(hostname, blockPrivate = true) {
276
304
  }
277
305
  for (const addr of addresses) {
278
306
  const ip = addr.address;
279
- if (ip === "169.254.169.254") {
307
+ const normIp = stripIpv6Mapped(ip);
308
+ if (normIp === "169.254.169.254") {
280
309
  return { safe: false, error: `SSRF protection: hostname ${hostname} resolves to cloud metadata IP ${ip}` };
281
310
  }
282
311
  if (blockPrivate && (isLoopbackIp(ip) || isPrivateIp(ip))) {
@@ -347,8 +376,15 @@ var INJECTION_PATTERNS = [
347
376
  ];
348
377
  function sanitizeCommand(command) {
349
378
  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: "" };
379
+ let normalizedCmd = command.normalize("NFKC");
380
+ normalizedCmd = normalizedCmd.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "");
381
+ if (normalizedCmd !== command.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "").normalize("NFKC")) {
382
+ debugLog("security", "command contained Unicode normalization variance (possible homoglyph bypass)", { original: command });
383
+ }
384
+ command = normalizedCmd;
385
+ const trimmed = command.trim();
386
+ if (!trimmed) return { isSafe: false, error: "Command cannot be empty", command: "" };
387
+ const parts = trimmed.split(/\s+/);
352
388
  let baseCmd = parts[0].toLowerCase();
353
389
  if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
354
390
  if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
@@ -437,6 +473,12 @@ function readRecentAuditEntries(count = 50) {
437
473
  return [];
438
474
  }
439
475
  }
476
+ process.on("exit", () => {
477
+ flushAuditBuffer();
478
+ });
479
+ process.on("SIGTERM", () => {
480
+ flushAuditBuffer();
481
+ });
440
482
  function checkBashToolInput(input) {
441
483
  const command = input.command ?? input.cmd ?? "";
442
484
  if (!command) return { safe: true, rule: "", detail: "" };
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.8";
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(model2, 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(`${model2} is a STRONG model${suffix} \u2014 full capability`));
75
+ } else if (passed > 0 && passed >= total - 1) {
76
+ lines.push(ok(`${model2} is a GOOD model${suffix} \u2014 most capabilities work`));
77
+ } else if (passed > 0 && passed >= total - 2) {
78
+ lines.push(warn(`${model2} is USABLE${suffix} \u2014 some capabilities are limited`));
79
+ } else {
80
+ lines.push(fail(`${model2} 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
+ };