@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 +49 -0
- package/errors.js +56 -0
- package/format.js +1 -0
- package/model-test-utils.js +4 -0
- package/ollama.js +6 -5
- package/package.json +1 -1
- package/provider-sync.js +18 -0
- package/react-parser.js +18 -0
- package/security.js +52 -10
- package/test-report.js +89 -0
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`;
|
package/model-test-utils.js
CHANGED
|
@@ -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.
|
|
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
|
|
250
|
-
if (!
|
|
251
|
-
const providerName =
|
|
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
package/provider-sync.js
ADDED
|
@@ -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 = ["/
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
if (
|
|
261
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
351
|
-
|
|
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
|
+
};
|