@vtstech/pi-shared 1.1.8 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config-io.js +11 -3
- package/errors.js +3 -3
- package/ollama.js +15 -6
- package/package.json +4 -1
- package/security.js +117 -36
- package/test-report.js +6 -6
package/config-io.js
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
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
|
|
5
15
|
var PI_AGENT_DIR = path.join(os.homedir(), ".pi", "agent");
|
|
6
16
|
function readJsonConfig(filePath, defaultValue = {}) {
|
|
7
17
|
try {
|
|
@@ -9,9 +19,7 @@ function readJsonConfig(filePath, defaultValue = {}) {
|
|
|
9
19
|
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
10
20
|
}
|
|
11
21
|
} catch (err) {
|
|
12
|
-
|
|
13
|
-
console.debug(`[config-io] Failed to read config: ${filePath}`, err);
|
|
14
|
-
}
|
|
22
|
+
debugLog("config-io", `failed to read config: ${filePath}`, err);
|
|
15
23
|
}
|
|
16
24
|
return defaultValue;
|
|
17
25
|
}
|
package/errors.js
CHANGED
|
@@ -24,11 +24,11 @@ var ApiError = class extends ExtensionError {
|
|
|
24
24
|
this.name = "ApiError";
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
var
|
|
27
|
+
var ExtensionTimeoutError = class extends ExtensionError {
|
|
28
28
|
constructor(message, timeoutMs) {
|
|
29
29
|
super(message, "TIMEOUT");
|
|
30
30
|
__publicField(this, "timeoutMs", timeoutMs);
|
|
31
|
-
this.name = "
|
|
31
|
+
this.name = "ExtensionTimeoutError";
|
|
32
32
|
}
|
|
33
33
|
};
|
|
34
34
|
var SecurityError = class extends ExtensionError {
|
|
@@ -50,7 +50,7 @@ export {
|
|
|
50
50
|
ApiError,
|
|
51
51
|
ConfigError,
|
|
52
52
|
ExtensionError,
|
|
53
|
+
ExtensionTimeoutError,
|
|
53
54
|
SecurityError,
|
|
54
|
-
TimeoutError,
|
|
55
55
|
ToolError
|
|
56
56
|
};
|
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.
|
|
15
|
+
var EXTENSION_VERSION = "1.2.0";
|
|
16
16
|
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
17
17
|
var _modelsJsonCache = null;
|
|
18
18
|
var _ollamaBaseUrlCache = null;
|
|
@@ -180,7 +180,7 @@ async function fetchModelContextLength(baseUrl, modelName) {
|
|
|
180
180
|
const numCtx = data?.model_info?.["num_ctx"];
|
|
181
181
|
if (typeof numCtx === "number") return numCtx;
|
|
182
182
|
} catch (err) {
|
|
183
|
-
debugLog("ollama", `failed to fetch context length for ${
|
|
183
|
+
debugLog("ollama", `failed to fetch context length for ${modelName}`, err);
|
|
184
184
|
return void 0;
|
|
185
185
|
}
|
|
186
186
|
return void 0;
|
|
@@ -214,7 +214,8 @@ var BUILTIN_PROVIDERS = {
|
|
|
214
214
|
xai: { api: "openai-completions", baseUrl: "https://api.x.ai/v1", envKey: "XAI_API_KEY" },
|
|
215
215
|
together: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1", envKey: "TOGETHER_API_KEY" },
|
|
216
216
|
fireworks: { api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", envKey: "FIREWORKS_API_KEY" },
|
|
217
|
-
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" }
|
|
218
219
|
};
|
|
219
220
|
function detectModelFamily(modelName) {
|
|
220
221
|
const name = modelName.toLowerCase();
|
|
@@ -234,6 +235,8 @@ function detectModelFamily(modelName) {
|
|
|
234
235
|
["gemma", "gemma2"],
|
|
235
236
|
["granite", "granite"],
|
|
236
237
|
["dolphin", "dolphin"],
|
|
238
|
+
["glm-4", "glm"],
|
|
239
|
+
["glm", "glm"],
|
|
237
240
|
["deepseek-r1", "deepseek-r1"],
|
|
238
241
|
["deepseek", "deepseek"],
|
|
239
242
|
["mistral", "qwen2"],
|
|
@@ -247,9 +250,9 @@ function detectModelFamily(modelName) {
|
|
|
247
250
|
return "unknown";
|
|
248
251
|
}
|
|
249
252
|
function detectProvider(ctx) {
|
|
250
|
-
const
|
|
251
|
-
if (!
|
|
252
|
-
const providerName =
|
|
253
|
+
const model = ctx.model;
|
|
254
|
+
if (!model) return { kind: "unknown", name: "none" };
|
|
255
|
+
const providerName = model.provider || "";
|
|
253
256
|
if (!providerName) return { kind: "unknown", name: "none" };
|
|
254
257
|
const modelsJson = readModelsJson();
|
|
255
258
|
const userProviderCfg = (modelsJson.providers || {})[providerName];
|
|
@@ -286,6 +289,11 @@ function detectProvider(ctx) {
|
|
|
286
289
|
}
|
|
287
290
|
return { kind: "unknown", name: providerName };
|
|
288
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
|
+
}
|
|
289
297
|
export {
|
|
290
298
|
BUILTIN_PROVIDERS,
|
|
291
299
|
EXTENSION_VERSION,
|
|
@@ -297,6 +305,7 @@ export {
|
|
|
297
305
|
fetchModelContextLength,
|
|
298
306
|
fetchOllamaModels,
|
|
299
307
|
getOllamaBaseUrl,
|
|
308
|
+
isLocalProvider,
|
|
300
309
|
isReasoningModel,
|
|
301
310
|
readModelsJson,
|
|
302
311
|
readModifyWriteModelsJson,
|
package/package.json
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vtstech/pi-shared",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Shared utilities for Pi Coding Agent extensions",
|
|
5
5
|
"exports": {
|
|
6
|
+
"./config-io": "./config-io.js",
|
|
6
7
|
"./debug": "./debug.js",
|
|
7
8
|
"./format": "./format.js",
|
|
8
9
|
"./model-test-utils": "./model-test-utils.js",
|
|
9
10
|
"./ollama": "./ollama.js",
|
|
11
|
+
"./provider-sync": "./provider-sync.js",
|
|
10
12
|
"./react-parser": "./react-parser.js",
|
|
11
13
|
"./security": "./security.js",
|
|
14
|
+
"./test-report": "./test-report.js",
|
|
12
15
|
"./types": "./types.js"
|
|
13
16
|
},
|
|
14
17
|
"keywords": ["pi-extensions"],
|
package/security.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// shared/security.ts
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
|
-
import * as
|
|
4
|
-
import
|
|
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
|
-
|
|
17
|
-
|
|
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 =
|
|
42
|
+
const configDir = path2.dirname(SECURITY_CONFIG_PATH);
|
|
32
43
|
try {
|
|
33
44
|
if (!fs.existsSync(configDir)) {
|
|
34
45
|
fs.mkdirSync(configDir, { recursive: true });
|
|
@@ -222,7 +233,7 @@ function validatePath(filePath, allowedDirs) {
|
|
|
222
233
|
}
|
|
223
234
|
let resolved;
|
|
224
235
|
try {
|
|
225
|
-
resolved =
|
|
236
|
+
resolved = path2.resolve(filePath);
|
|
226
237
|
try {
|
|
227
238
|
resolved = fs.realpathSync(resolved);
|
|
228
239
|
} catch {
|
|
@@ -240,9 +251,9 @@ function validatePath(filePath, allowedDirs) {
|
|
|
240
251
|
"/etc/passwd",
|
|
241
252
|
"/.ssh/",
|
|
242
253
|
"/.gnupg/",
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
path2.join(os2.homedir(), ".ssh"),
|
|
255
|
+
path2.join(os2.homedir(), ".gnupg"),
|
|
256
|
+
SETTINGS_PATH2,
|
|
246
257
|
SECURITY_CONFIG_PATH
|
|
247
258
|
// NOTE: models.json is intentionally excluded from sensitivePaths.
|
|
248
259
|
// Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
|
|
@@ -262,7 +273,7 @@ function validatePath(filePath, allowedDirs) {
|
|
|
262
273
|
if (allowedDirs) {
|
|
263
274
|
for (const dir of allowedDirs) {
|
|
264
275
|
try {
|
|
265
|
-
const absDir =
|
|
276
|
+
const absDir = path2.resolve(dir);
|
|
266
277
|
if (resolved.startsWith(absDir)) return { valid: true, error: "" };
|
|
267
278
|
} catch {
|
|
268
279
|
}
|
|
@@ -345,12 +356,24 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
345
356
|
const mode = getSecurityMode();
|
|
346
357
|
for (const pattern of BLOCKED_URL_ALWAYS) {
|
|
347
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
|
+
}
|
|
348
365
|
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
|
|
349
366
|
}
|
|
350
367
|
}
|
|
351
368
|
if (mode === "max") {
|
|
352
369
|
for (const pattern of BLOCKED_URL_MAX_ONLY) {
|
|
353
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
|
+
}
|
|
354
377
|
return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}' (max mode)` };
|
|
355
378
|
}
|
|
356
379
|
}
|
|
@@ -359,31 +382,19 @@ function isSafeUrl(url, blockSsrf = true) {
|
|
|
359
382
|
return { safe: true, error: "" };
|
|
360
383
|
}
|
|
361
384
|
var INJECTION_PATTERNS = [
|
|
362
|
-
//
|
|
385
|
+
// Semicolon chaining to dangerous commands — mode-independent.
|
|
386
|
+
// Unlike && (conditional), ; ALWAYS runs the second command.
|
|
363
387
|
/;\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill|pkill)\b/i,
|
|
364
|
-
// Piping to dangerous commands
|
|
365
|
-
/\|\s*(rm|sudo|chmod|chown|shred|mkfs)\b/i,
|
|
366
|
-
// AND chaining with dangerous commands
|
|
367
|
-
/&&\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill)\b/i,
|
|
368
388
|
// Command substitution (backticks) — still dangerous
|
|
369
389
|
/`[^`]+`/,
|
|
370
390
|
// Command substitution ($()) — still dangerous
|
|
371
391
|
/\$\([^)]+\)/,
|
|
372
392
|
// Variable expansion targeting sensitive env vars
|
|
373
|
-
/\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
|
|
374
|
-
// Bare pipe without space (likely injection, not intentional piping)
|
|
375
|
-
/\|(?=[^\s|])/
|
|
393
|
+
/\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
|
|
376
394
|
];
|
|
377
|
-
function
|
|
378
|
-
if (!command) 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;
|
|
395
|
+
function checkSingleCommand(command, mode) {
|
|
385
396
|
const trimmed = command.trim();
|
|
386
|
-
if (!trimmed) return { isSafe:
|
|
397
|
+
if (!trimmed) return { isSafe: true, error: "", command: "" };
|
|
387
398
|
const parts = trimmed.split(/\s+/);
|
|
388
399
|
let baseCmd = parts[0].toLowerCase();
|
|
389
400
|
if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
|
|
@@ -396,23 +407,57 @@ function sanitizeCommand(command) {
|
|
|
396
407
|
return { isSafe: false, error: `Blocked command: ${word} (critical)`, command: "" };
|
|
397
408
|
}
|
|
398
409
|
}
|
|
399
|
-
const mode = getSecurityMode();
|
|
400
410
|
if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
|
|
401
411
|
return { isSafe: false, error: `Blocked command: ${baseCmd} (max mode)`, command: "" };
|
|
402
412
|
}
|
|
403
|
-
const
|
|
404
|
-
|
|
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) {
|
|
405
433
|
return { isSafe: false, error: "Newline characters detected: potential command injection", command: "" };
|
|
406
434
|
}
|
|
407
435
|
for (const pattern of INJECTION_PATTERNS) {
|
|
408
|
-
if (pattern.test(
|
|
436
|
+
if (pattern.test(trimmed)) {
|
|
409
437
|
return { isSafe: false, error: `Potential injection pattern detected`, command: "" };
|
|
410
438
|
}
|
|
411
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
|
+
}
|
|
412
457
|
return { isSafe: true, error: "", command };
|
|
413
458
|
}
|
|
414
|
-
var AUDIT_DIR =
|
|
415
|
-
var AUDIT_LOG_PATH =
|
|
459
|
+
var AUDIT_DIR = path2.join(os2.homedir(), ".pi", "agent");
|
|
460
|
+
var AUDIT_LOG_PATH = path2.join(AUDIT_DIR, "audit.log");
|
|
416
461
|
var AUDIT_BUFFER_MAX_ENTRIES = 50;
|
|
417
462
|
var AUDIT_FLUSH_INTERVAL_MS = 500;
|
|
418
463
|
var _auditBuffer = [];
|
|
@@ -445,6 +490,19 @@ function flushAuditBuffer() {
|
|
|
445
490
|
function appendAuditEntry(entry) {
|
|
446
491
|
try {
|
|
447
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
|
+
}
|
|
448
506
|
const enriched = { ...entry, securityMode: getSecurityMode() };
|
|
449
507
|
const line = JSON.stringify(enriched) + "\n";
|
|
450
508
|
_auditBuffer.push(line);
|
|
@@ -458,8 +516,31 @@ function appendAuditEntry(entry) {
|
|
|
458
516
|
function readRecentAuditEntries(count = 50) {
|
|
459
517
|
try {
|
|
460
518
|
if (!fs.existsSync(AUDIT_LOG_PATH)) return [];
|
|
461
|
-
const
|
|
462
|
-
|
|
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
|
+
}
|
|
463
544
|
const recent = lines.slice(-count);
|
|
464
545
|
return recent.map((line) => {
|
|
465
546
|
try {
|
|
@@ -543,7 +624,7 @@ export {
|
|
|
543
624
|
CRITICAL_COMMANDS,
|
|
544
625
|
EXTENDED_COMMANDS,
|
|
545
626
|
SECURITY_CONFIG_PATH,
|
|
546
|
-
SETTINGS_PATH,
|
|
627
|
+
SETTINGS_PATH2 as SETTINGS_PATH,
|
|
547
628
|
appendAuditEntry,
|
|
548
629
|
checkBashToolInput,
|
|
549
630
|
checkFileToolInput,
|
package/test-report.js
CHANGED
|
@@ -29,7 +29,7 @@ import os from "node:os";
|
|
|
29
29
|
var DEBUG_ENABLED = process.env.PI_EXTENSIONS_DEBUG === "1";
|
|
30
30
|
|
|
31
31
|
// shared/ollama.ts
|
|
32
|
-
var EXTENSION_VERSION = "1.
|
|
32
|
+
var EXTENSION_VERSION = "1.2.0";
|
|
33
33
|
var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
|
|
34
34
|
|
|
35
35
|
// shared/test-report.ts
|
|
@@ -66,18 +66,18 @@ function formatTestSummary(tests, totalMs) {
|
|
|
66
66
|
lines.push(info(`Score: ${passed}/${tests.length} tests passed`));
|
|
67
67
|
return lines;
|
|
68
68
|
}
|
|
69
|
-
function formatRecommendation(
|
|
69
|
+
function formatRecommendation(model, passed, total, via) {
|
|
70
70
|
const suffix = via ? ` via ${via}` : "";
|
|
71
71
|
const lines = [];
|
|
72
72
|
lines.push(section("RECOMMENDATION"));
|
|
73
73
|
if (passed === total) {
|
|
74
|
-
lines.push(ok(`${
|
|
74
|
+
lines.push(ok(`${model} is a STRONG model${suffix} \u2014 full capability`));
|
|
75
75
|
} else if (passed > 0 && passed >= total - 1) {
|
|
76
|
-
lines.push(ok(`${
|
|
76
|
+
lines.push(ok(`${model} is a GOOD model${suffix} \u2014 most capabilities work`));
|
|
77
77
|
} else if (passed > 0 && passed >= total - 2) {
|
|
78
|
-
lines.push(warn(`${
|
|
78
|
+
lines.push(warn(`${model} is USABLE${suffix} \u2014 some capabilities are limited`));
|
|
79
79
|
} else {
|
|
80
|
-
lines.push(fail(`${
|
|
80
|
+
lines.push(fail(`${model} is WEAK${suffix} \u2014 limited capabilities for agent use`));
|
|
81
81
|
}
|
|
82
82
|
return lines;
|
|
83
83
|
}
|