@vtstech/pi-diag 1.2.1 → 1.2.3

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.
Files changed (2) hide show
  1. package/diag.js +617 -44
  2. package/package.json +10 -6
package/diag.js CHANGED
@@ -1,34 +1,607 @@
1
- // .build-npm/diag/diag.temp.ts
1
+ // extensions/diag.ts
2
+ import * as fs4 from "node:fs";
3
+ import * as os4 from "node:os";
4
+ import * as path4 from "node:path";
5
+
6
+ // shared/format.ts
7
+ function section(title) {
8
+ return `
9
+ \u2500\u2500 ${title} ${"\u2500".repeat(Math.max(1, 60 - title.length - 4))}`;
10
+ }
11
+ function ok(msg) {
12
+ return ` \u2705 ${msg}`;
13
+ }
14
+ function fail(msg) {
15
+ return ` \u274C ${msg}`;
16
+ }
17
+ function warn(msg) {
18
+ return ` \u26A0\uFE0F ${msg}`;
19
+ }
20
+ function info(msg) {
21
+ return ` \u2139\uFE0F ${msg}`;
22
+ }
23
+ function bytesHuman(bytes) {
24
+ const units = ["B", "KB", "MB", "GB", "TB"];
25
+ let i = 0;
26
+ let b = bytes;
27
+ while (b >= 1024 && i < units.length - 1) {
28
+ b /= 1024;
29
+ i++;
30
+ }
31
+ return `${b.toFixed(1)}${units[i]}`;
32
+ }
33
+ function msHuman(ms) {
34
+ if (ms < 1e3) return `${ms.toFixed(0)}ms`;
35
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
36
+ return `${(ms / 6e4).toFixed(1)}m`;
37
+ }
38
+ function pct(used, total) {
39
+ if (total === 0) return "0.0%";
40
+ return `${(used / total * 100).toFixed(1)}%`;
41
+ }
42
+
43
+ // shared/ollama.ts
2
44
  import * as fs from "node:fs";
3
- import * as os from "node:os";
4
45
  import * as path from "node:path";
5
- import {
6
- section,
7
- ok,
8
- fail,
9
- warn,
10
- info,
11
- bytesHuman,
12
- msHuman,
13
- pct
14
- } from "@vtstech/pi-shared/format";
15
- import { MODELS_JSON_PATH, getOllamaBaseUrl, BUILTIN_PROVIDERS, readModelsJson, EXTENSION_VERSION, isLocalProvider } from "@vtstech/pi-shared/ollama";
16
- import {
17
- BLOCKED_COMMANDS,
18
- BLOCKED_URL_PATTERNS,
19
- CRITICAL_COMMANDS,
20
- EXTENDED_COMMANDS,
21
- BLOCKED_URL_ALWAYS,
22
- BLOCKED_URL_MAX_ONLY,
23
- getSecurityMode,
24
- validatePath,
25
- isSafeUrl,
26
- sanitizeCommand,
27
- readRecentAuditEntries,
28
- AUDIT_LOG_PATH
29
- } from "@vtstech/pi-shared/security";
30
- import { readSettings } from "@vtstech/pi-shared/config-io";
31
- import { debugLog } from "@vtstech/pi-shared/debug";
46
+ import os from "node:os";
47
+
48
+ // shared/debug.ts
49
+ var DEBUG_ENABLED = process?.env?.PI_EXTENSIONS_DEBUG === "1";
50
+ function debugLog(module, message, ...args) {
51
+ if (!DEBUG_ENABLED) return;
52
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
53
+ console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
54
+ }
55
+
56
+ // shared/ollama.ts
57
+ var EXTENSION_VERSION = "1.2.3";
58
+ var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
59
+ var _modelsJsonCache = null;
60
+ var _ollamaBaseUrlCache = null;
61
+ var CACHE_TTL_MS = 2e3;
62
+ function getOllamaBaseUrl() {
63
+ const now = Date.now();
64
+ if (_ollamaBaseUrlCache && now - _ollamaBaseUrlCache.ts < CACHE_TTL_MS) return _ollamaBaseUrlCache.data;
65
+ try {
66
+ if (fs.existsSync(MODELS_JSON_PATH)) {
67
+ const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
68
+ const config = JSON.parse(raw);
69
+ const baseUrl = config?.providers?.["ollama"]?.baseUrl;
70
+ if (baseUrl) {
71
+ const result = baseUrl.replace(/\/v1\/?$/, "");
72
+ _ollamaBaseUrlCache = { data: result, ts: now };
73
+ return result;
74
+ }
75
+ }
76
+ } catch (err) {
77
+ debugLog("ollama", "failed to parse models.json for base URL", err);
78
+ }
79
+ if (process.env.OLLAMA_HOST) {
80
+ const result = `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, "")}`;
81
+ _ollamaBaseUrlCache = { data: result, ts: now };
82
+ return result;
83
+ }
84
+ const fallback = "http://localhost:11434";
85
+ _ollamaBaseUrlCache = { data: fallback, ts: now };
86
+ return fallback;
87
+ }
88
+ function readModelsJson() {
89
+ const now = Date.now();
90
+ if (_modelsJsonCache && now - _modelsJsonCache.ts < CACHE_TTL_MS) return _modelsJsonCache.data;
91
+ try {
92
+ if (fs.existsSync(MODELS_JSON_PATH)) {
93
+ const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
94
+ const data = JSON.parse(raw);
95
+ _modelsJsonCache = { data, ts: now };
96
+ return data;
97
+ }
98
+ } catch (err) {
99
+ debugLog("ollama", "failed to read/parse models.json", err);
100
+ }
101
+ const empty = { providers: {} };
102
+ _modelsJsonCache = { data: empty, ts: now };
103
+ return empty;
104
+ }
105
+ var BUILTIN_PROVIDERS = {
106
+ openrouter: { api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1", envKey: "OPENROUTER_API_KEY" },
107
+ anthropic: { api: "anthropic-messages", baseUrl: "https://api.anthropic.com/v1", envKey: "ANTHROPIC_API_KEY" },
108
+ google: { api: "gemini", baseUrl: "https://generativelanguage.googleapis.com", envKey: "GOOGLE_API_KEY" },
109
+ openai: { api: "openai-completions", baseUrl: "https://api.openai.com/v1", envKey: "OPENAI_API_KEY" },
110
+ groq: { api: "openai-completions", baseUrl: "https://api.groq.com/v1", envKey: "GROQ_API_KEY" },
111
+ deepseek: { api: "openai-completions", baseUrl: "https://api.deepseek.com/v1", envKey: "DEEPSEEK_API_KEY" },
112
+ mistral: { api: "openai-completions", baseUrl: "https://api.mistral.ai/v1", envKey: "MISTRAL_API_KEY" },
113
+ xai: { api: "openai-completions", baseUrl: "https://api.x.ai/v1", envKey: "XAI_API_KEY" },
114
+ together: { api: "openai-completions", baseUrl: "https://api.together.xyz/v1", envKey: "TOGETHER_API_KEY" },
115
+ fireworks: { api: "openai-completions", baseUrl: "https://api.fireworks.ai/inference/v1", envKey: "FIREWORKS_API_KEY" },
116
+ cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com/v1", envKey: "COHERE_API_KEY" },
117
+ zai: { api: "openai-completions", baseUrl: "https://open.bigmodel.cn/api/paas/v4", envKey: "ZAI_API_KEY" }
118
+ };
119
+ function isLocalProvider(baseUrl, providerName) {
120
+ if (providerName === "ollama") return true;
121
+ const url = baseUrl || "";
122
+ return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0");
123
+ }
124
+
125
+ // shared/security.ts
126
+ import * as fs3 from "node:fs";
127
+ import * as path3 from "node:path";
128
+ import os3 from "node:os";
129
+
130
+ // shared/config-io.ts
131
+ import * as fs2 from "fs";
132
+ import * as path2 from "path";
133
+ import os2 from "os";
134
+ var PI_AGENT_DIR = path2.join(os2.homedir(), ".pi", "agent");
135
+ function readJsonConfig(filePath, defaultValue = {}) {
136
+ try {
137
+ if (fs2.existsSync(filePath)) {
138
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
139
+ }
140
+ } catch (err) {
141
+ debugLog("config-io", `failed to read config: ${filePath}`, err);
142
+ }
143
+ return defaultValue;
144
+ }
145
+ var SETTINGS_PATH = path2.join(PI_AGENT_DIR, "settings.json");
146
+ var SECURITY_PATH = path2.join(PI_AGENT_DIR, "security.json");
147
+ var REACT_MODE_PATH = path2.join(PI_AGENT_DIR, "react-mode.json");
148
+ var MODEL_TEST_CONFIG_PATH = path2.join(PI_AGENT_DIR, "model-test-config.json");
149
+ function readSettings() {
150
+ return readJsonConfig(SETTINGS_PATH);
151
+ }
152
+
153
+ // shared/security.ts
154
+ var SETTINGS_PATH2 = SETTINGS_PATH;
155
+ var SECURITY_CONFIG_PATH = SECURITY_PATH;
156
+ var securityModeCache = null;
157
+ var securityModeCacheTime = 0;
158
+ var SECURITY_CACHE_DURATION_MS = 3e4;
159
+ function getSecurityMode() {
160
+ const now = Date.now();
161
+ if (securityModeCache && now - securityModeCacheTime < SECURITY_CACHE_DURATION_MS) {
162
+ return securityModeCache;
163
+ }
164
+ try {
165
+ if (!fs3.existsSync(SECURITY_CONFIG_PATH)) {
166
+ securityModeCache = "max";
167
+ securityModeCacheTime = now;
168
+ return "max";
169
+ }
170
+ const raw = fs3.readFileSync(SECURITY_CONFIG_PATH, "utf-8");
171
+ const config = JSON.parse(raw);
172
+ if (config.mode === "basic" || config.mode === "max" || config.mode === "off") {
173
+ securityModeCache = config.mode;
174
+ securityModeCacheTime = now;
175
+ return config.mode;
176
+ }
177
+ securityModeCache = "max";
178
+ securityModeCacheTime = now;
179
+ return "max";
180
+ } catch (err) {
181
+ debugLog("security", `failed to read security config at ${SECURITY_CONFIG_PATH}`, err);
182
+ securityModeCache = "max";
183
+ securityModeCacheTime = now;
184
+ return "max";
185
+ }
186
+ }
187
+ var CRITICAL_COMMANDS = /* @__PURE__ */ new Set([
188
+ // Filesystem destruction (irrecoverable)
189
+ "mkfs",
190
+ "dd",
191
+ "shred",
192
+ "wipe",
193
+ "srm",
194
+ "format",
195
+ "fdisk",
196
+ // Privilege escalation (non-sudo)
197
+ "su",
198
+ "doas",
199
+ "pkexec",
200
+ "gksudo",
201
+ "kdesu",
202
+ // Network attack tools
203
+ "nmap",
204
+ "nc",
205
+ "netcat",
206
+ "telnet",
207
+ // Remote access
208
+ "ssh",
209
+ "scp",
210
+ "sftp",
211
+ "rsync",
212
+ // Process killing
213
+ "kill",
214
+ "killall",
215
+ "pkill",
216
+ "xkill",
217
+ // User management
218
+ "useradd",
219
+ "userdel",
220
+ "usermod",
221
+ "passwd",
222
+ "adduser",
223
+ "deluser",
224
+ // Dangerous shell features
225
+ "exec",
226
+ "eval",
227
+ "source",
228
+ ".",
229
+ "alias",
230
+ // Filesystem control
231
+ "mount",
232
+ "umount",
233
+ "chattr",
234
+ "lsattr",
235
+ // Permission modification
236
+ "chown",
237
+ "chmod"
238
+ ]);
239
+ var EXTENDED_COMMANDS = /* @__PURE__ */ new Set([
240
+ // File deletion
241
+ "rm",
242
+ "rmdir",
243
+ "del",
244
+ // Privilege escalation
245
+ "sudo",
246
+ // Download tools
247
+ "wget",
248
+ "curl",
249
+ // Package management
250
+ "apt",
251
+ "apt-get",
252
+ "yum",
253
+ "dnf",
254
+ "pacman",
255
+ "pip",
256
+ "npm",
257
+ "yarn",
258
+ "cargo",
259
+ // System service control
260
+ "systemctl",
261
+ "service",
262
+ // Interactive editors (shell escape risk)
263
+ "vi",
264
+ "vim",
265
+ "nano",
266
+ "emacs",
267
+ "less",
268
+ "more",
269
+ "man",
270
+ // Version control
271
+ "git"
272
+ ]);
273
+ var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
274
+ ...CRITICAL_COMMANDS,
275
+ ...EXTENDED_COMMANDS
276
+ ]);
277
+ var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
278
+ // Cloud metadata endpoints
279
+ "169.254.169.254",
280
+ // RFC1918 private ranges
281
+ "10.",
282
+ "192.168.",
283
+ "172.16.",
284
+ "172.17.",
285
+ "172.18.",
286
+ "172.19.",
287
+ "172.20.",
288
+ "172.21.",
289
+ "172.22.",
290
+ "172.23.",
291
+ "172.24.",
292
+ "172.25.",
293
+ "172.26.",
294
+ "172.27.",
295
+ "172.28.",
296
+ "172.29.",
297
+ "172.30.",
298
+ "172.31.",
299
+ // IPv6-mapped IPv4 cloud metadata (always blocked)
300
+ "::ffff:169.254.169.254",
301
+ // Internal service patterns
302
+ "internal.",
303
+ "private.",
304
+ "intranet."
305
+ ]);
306
+ var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
307
+ // Loopback addresses (full 127.0.0.0/8 range)
308
+ "localhost",
309
+ "127.",
310
+ "0.0.0.0",
311
+ "::1",
312
+ "::ffff:127.0.0.1",
313
+ "::ffff:0.0.0.0",
314
+ // IPv6-mapped IPv4 private ranges (always blocked in max mode)
315
+ "::ffff:10.",
316
+ "::ffff:192.168.",
317
+ "::ffff:172.16.",
318
+ "::ffff:172.17.",
319
+ "::ffff:172.18.",
320
+ "::ffff:172.19.",
321
+ "::ffff:172.20.",
322
+ "::ffff:172.21.",
323
+ "::ffff:172.22.",
324
+ "::ffff:172.23.",
325
+ "::ffff:172.24.",
326
+ "::ffff:172.25.",
327
+ "::ffff:172.26.",
328
+ "::ffff:172.27.",
329
+ "::ffff:172.28.",
330
+ "::ffff:172.29.",
331
+ "::ffff:172.30.",
332
+ "::ffff:172.31.",
333
+ // Local/internal patterns
334
+ "local."
335
+ ]);
336
+ var BLOCKED_URL_PATTERNS = /* @__PURE__ */ new Set([
337
+ ...BLOCKED_URL_ALWAYS,
338
+ ...BLOCKED_URL_MAX_ONLY
339
+ ]);
340
+ var CRITICAL_SYSTEM_DIRS = [
341
+ "/etc",
342
+ "/root",
343
+ "/var",
344
+ "/usr",
345
+ "/bin",
346
+ "/sbin",
347
+ "/boot",
348
+ "/dev",
349
+ "/proc",
350
+ "/sys"
351
+ ];
352
+ function validatePath(filePath, allowedDirs) {
353
+ if (!filePath) return { valid: false, error: "Path cannot be empty" };
354
+ if (filePath.startsWith("\\\\")) {
355
+ return { valid: false, error: "UNC paths not allowed" };
356
+ }
357
+ if (filePath.includes("../") || filePath.includes("..\\")) {
358
+ return { valid: false, error: "Path traversal detected: parent directory access not allowed" };
359
+ }
360
+ let resolved;
361
+ try {
362
+ resolved = path3.resolve(filePath);
363
+ try {
364
+ resolved = fs3.realpathSync(resolved);
365
+ } catch {
366
+ }
367
+ } catch {
368
+ return { valid: false, error: "Invalid path format" };
369
+ }
370
+ for (const critical of CRITICAL_SYSTEM_DIRS) {
371
+ if (resolved.startsWith(critical + "/") || resolved === critical) {
372
+ return { valid: false, error: `Access to system directory denied: ${critical}` };
373
+ }
374
+ }
375
+ const sensitivePaths = [
376
+ "/etc/shadow",
377
+ "/etc/passwd",
378
+ "/.ssh/",
379
+ "/.gnupg/",
380
+ path3.join(os3.homedir(), ".ssh"),
381
+ path3.join(os3.homedir(), ".gnupg"),
382
+ SETTINGS_PATH2,
383
+ SECURITY_CONFIG_PATH
384
+ // NOTE: models.json is intentionally excluded from sensitivePaths.
385
+ // Extensions use readModelsJson()/writeModelsJson() from shared/ollama.ts
386
+ // for direct file I/O — not via Pi's tool system — so blocking it here
387
+ // would prevent legitimate model configuration updates.
388
+ ];
389
+ for (const sensitive of sensitivePaths) {
390
+ if (resolved.startsWith(sensitive) || resolved === sensitive) {
391
+ return { valid: false, error: `Access to sensitive path denied: ${sensitive}` };
392
+ }
393
+ }
394
+ const cwd = process.cwd();
395
+ const safePrefixes = ["/home", "/tmp", cwd];
396
+ for (const prefix of safePrefixes) {
397
+ if (resolved.startsWith(prefix + "/") || resolved === prefix) return { valid: true, error: "" };
398
+ }
399
+ if (allowedDirs) {
400
+ for (const dir of allowedDirs) {
401
+ try {
402
+ const absDir = path3.resolve(dir);
403
+ if (resolved.startsWith(absDir)) return { valid: true, error: "" };
404
+ } catch {
405
+ }
406
+ }
407
+ }
408
+ return { valid: false, error: `Path not in allowed directories: ${filePath}` };
409
+ }
410
+ function isSafeUrl(url, blockSsrf = true, mode = "max") {
411
+ if (!url) return { safe: false, error: "URL cannot be empty" };
412
+ let parsed;
413
+ try {
414
+ parsed = new URL(url);
415
+ } catch (e) {
416
+ const msg = e instanceof Error ? e.message : String(e);
417
+ return { safe: false, error: `Invalid URL format: ${msg}` };
418
+ }
419
+ const scheme = parsed.protocol.replace(":", "").toLowerCase();
420
+ if (scheme !== "http" && scheme !== "https") {
421
+ return { safe: false, error: `URL scheme not allowed: ${parsed.protocol}` };
422
+ }
423
+ if (!parsed.hostname) {
424
+ return { safe: false, error: "URL must have a hostname" };
425
+ }
426
+ const hostname = parsed.hostname.toLowerCase();
427
+ const normalized = hostname.replace(/\.$/, "");
428
+ if (/[^\x00-\x7F]/.test(normalized)) {
429
+ return { safe: false, error: "URL hostname contains non-ASCII characters" };
430
+ }
431
+ if (/^0x[0-9a-f]+$/i.test(normalized) || /^0[0-7]+$/i.test(normalized)) {
432
+ return { safe: false, error: "URL hostname uses non-decimal IP format" };
433
+ }
434
+ if (blockSsrf) {
435
+ if (mode === "off") {
436
+ return { safe: true, error: "" };
437
+ }
438
+ for (const pattern of BLOCKED_URL_ALWAYS) {
439
+ if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
440
+ if (/^\d|^::/.test(pattern)) {
441
+ const nextChar = normalized[pattern.length];
442
+ if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
443
+ continue;
444
+ }
445
+ }
446
+ return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}'` };
447
+ }
448
+ }
449
+ if (mode === "max") {
450
+ for (const pattern of BLOCKED_URL_MAX_ONLY) {
451
+ if (normalized === pattern || normalized.endsWith("." + pattern) || normalized.startsWith(pattern)) {
452
+ if (/^\d|^::/.test(pattern)) {
453
+ const nextChar = normalized[pattern.length];
454
+ if (nextChar && nextChar !== "/" && nextChar !== ":" && !/\d/.test(nextChar)) {
455
+ continue;
456
+ }
457
+ }
458
+ return { safe: false, error: `SSRF protection: blocked hostname pattern '${pattern}' (max mode)` };
459
+ }
460
+ }
461
+ }
462
+ }
463
+ return { safe: true, error: "" };
464
+ }
465
+ var INJECTION_PATTERNS = [
466
+ // Semicolon chaining to dangerous commands — mode-independent.
467
+ // Unlike && (conditional), ; ALWAYS runs the second command.
468
+ /;\s*(rm|sudo|chmod|chown|mkfs|dd|shred|kill|pkill)\b/i,
469
+ // Command substitution (backticks) — still dangerous
470
+ /`[^`]+`/,
471
+ // Command substitution ($()) — still dangerous
472
+ /\$\([^)]+\)/,
473
+ // Variable expansion targeting sensitive env vars
474
+ /\$\{?(?:HOME|USER|PATH|SHELL|PWD|SSH|GPG|API_KEY|TOKEN|SECRET|PASSWORD)\}?/i
475
+ ];
476
+ function checkSingleCommand(command, mode) {
477
+ const trimmed = command.trim();
478
+ if (!trimmed) return { isSafe: true, error: "", command: "" };
479
+ const parts = trimmed.split(/\s+/);
480
+ let baseCmd = parts[0].toLowerCase();
481
+ if (baseCmd.includes("/")) baseCmd = baseCmd.split("/").pop();
482
+ if (baseCmd.includes("\\")) baseCmd = baseCmd.split("\\").pop();
483
+ for (const raw of parts) {
484
+ let word = raw.toLowerCase();
485
+ if (word.includes("/")) word = word.split("/").pop();
486
+ if (word.includes("\\")) word = word.split("\\").pop();
487
+ if (CRITICAL_COMMANDS.has(word)) {
488
+ return { isSafe: false, error: `Blocked command: ${word} (critical)`, command: "" };
489
+ }
490
+ }
491
+ if (mode === "max" && EXTENDED_COMMANDS.has(baseCmd)) {
492
+ return { isSafe: false, error: `Blocked command: ${baseCmd} (max mode)`, command: "" };
493
+ }
494
+ for (const pattern of INJECTION_PATTERNS) {
495
+ if (pattern.test(trimmed)) {
496
+ return { isSafe: false, error: `Potential injection pattern detected in: ${trimmed}`, command: "" };
497
+ }
498
+ }
499
+ return { isSafe: true, error: "", command: trimmed };
500
+ }
501
+ function sanitizeCommand(command) {
502
+ if (!command) return { isSafe: false, error: "Command cannot be empty", command: "" };
503
+ let normalizedCmd = command.normalize("NFKC");
504
+ normalizedCmd = normalizedCmd.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "");
505
+ const strippedForCompare = command.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u2028-\u202e\ufeff\u2060-\u2069]/g, "").normalize("NFKC");
506
+ if (normalizedCmd !== strippedForCompare) {
507
+ return { isSafe: false, error: `Command rejected: Unicode normalization variance detected (possible homoglyph bypass)`, command: "" };
508
+ }
509
+ command = normalizedCmd;
510
+ const trimmed = command.trim();
511
+ if (!trimmed) return { isSafe: false, error: "Command cannot be empty", command: "" };
512
+ const newlineStripped = command.replace(/\n/g, " ").replace(/\r/g, " ");
513
+ if (newlineStripped !== command) {
514
+ return { isSafe: false, error: "Newline characters detected: potential command injection", command: "" };
515
+ }
516
+ for (const pattern of INJECTION_PATTERNS) {
517
+ if (pattern.test(trimmed)) {
518
+ return { isSafe: false, error: `Potential injection pattern detected`, command: "" };
519
+ }
520
+ }
521
+ const subCommands = [];
522
+ let remaining = trimmed;
523
+ const chainRegex = /&&|\|\||(?<!\|)\|(?!\|)/g;
524
+ let match;
525
+ let lastIndex = 0;
526
+ while ((match = chainRegex.exec(remaining)) !== null) {
527
+ subCommands.push(remaining.slice(lastIndex, match.index));
528
+ lastIndex = match.index + match[0].length;
529
+ }
530
+ subCommands.push(remaining.slice(lastIndex));
531
+ const mode = getSecurityMode();
532
+ for (const subCmd of subCommands) {
533
+ const result = checkSingleCommand(subCmd, mode);
534
+ if (!result.isSafe) {
535
+ return { isSafe: false, error: result.error, command: "" };
536
+ }
537
+ }
538
+ return { isSafe: true, error: "", command };
539
+ }
540
+ var AUDIT_DIR = path3.join(os3.homedir(), ".pi", "agent");
541
+ var AUDIT_LOG_PATH = path3.join(AUDIT_DIR, "audit.log");
542
+ var _auditBuffer = [];
543
+ function flushAuditBuffer() {
544
+ if (_auditBuffer.length === 0) return;
545
+ try {
546
+ if (!fs3.existsSync(AUDIT_DIR)) {
547
+ fs3.mkdirSync(AUDIT_DIR, { recursive: true });
548
+ }
549
+ const batch = _auditBuffer.join("");
550
+ fs3.appendFileSync(AUDIT_LOG_PATH, batch, "utf-8");
551
+ } catch (err) {
552
+ debugLog("security", "audit buffer flush failure", err);
553
+ }
554
+ _auditBuffer = [];
555
+ }
556
+ function readRecentAuditEntries(count = 50) {
557
+ try {
558
+ if (!fs3.existsSync(AUDIT_LOG_PATH)) return [];
559
+ const fileSize = fs3.statSync(AUDIT_LOG_PATH).size;
560
+ if (fileSize === 0) return [];
561
+ const fd = fs3.openSync(AUDIT_LOG_PATH, "r");
562
+ const bufferSize = 8192;
563
+ const buffer = Buffer.alloc(bufferSize);
564
+ const lines = [];
565
+ let pos = fileSize;
566
+ let partial = "";
567
+ while (pos > 0 && lines.length < count) {
568
+ const readSize = Math.min(bufferSize, pos);
569
+ pos -= readSize;
570
+ fs3.readSync(fd, buffer, 0, readSize, pos);
571
+ const chunk = buffer.slice(0, readSize).toString("utf-8");
572
+ partial = chunk + partial;
573
+ const lineBreak = partial.lastIndexOf("\n");
574
+ if (lineBreak !== -1) {
575
+ const complete = partial.slice(lineBreak + 1);
576
+ if (complete.trim()) lines.unshift(complete);
577
+ partial = partial.slice(0, lineBreak);
578
+ }
579
+ }
580
+ fs3.closeSync(fd);
581
+ if (partial.trim() && lines.length < count) {
582
+ lines.unshift(partial);
583
+ }
584
+ const recent = lines.slice(-count);
585
+ return recent.map((line) => {
586
+ try {
587
+ return JSON.parse(line);
588
+ } catch {
589
+ return {};
590
+ }
591
+ });
592
+ } catch (err) {
593
+ debugLog("security", "failed to read audit log", err);
594
+ return [];
595
+ }
596
+ }
597
+ process.on("exit", () => {
598
+ flushAuditBuffer();
599
+ });
600
+ process.on("SIGTERM", () => {
601
+ flushAuditBuffer();
602
+ });
603
+
604
+ // extensions/diag.ts
32
605
  var SECRET_KEY_PATTERNS = [
33
606
  /key/i,
34
607
  /token/i,
@@ -45,7 +618,7 @@ function redactValue(key, value) {
45
618
  if (value.length > 20 && !value.includes(" ") && /^[A-Za-z0-9_\-+/=]+$/.test(value)) return value.slice(0, 8) + "...";
46
619
  return value;
47
620
  }
48
- function diag_temp_default(pi) {
621
+ function diag_default(pi) {
49
622
  let cachedSystemPrompt = null;
50
623
  let cachedPayload = null;
51
624
  pi.on("before_provider_request", (event) => {
@@ -79,15 +652,15 @@ function diag_temp_default(pi) {
79
652
  }
80
653
  };
81
654
  lines.push(section("SYSTEM"));
82
- const cpus2 = os.cpus();
83
- const totalMem = os.totalmem();
84
- const freeMem = os.freemem();
655
+ const cpus2 = os4.cpus();
656
+ const totalMem = os4.totalmem();
657
+ const freeMem = os4.freemem();
85
658
  const usedMem = totalMem - freeMem;
86
659
  const memPct = pct(usedMem, totalMem);
87
- lines.push(info(`OS: ${os.type()} ${os.release()} ${os.arch()}`));
660
+ lines.push(info(`OS: ${os4.type()} ${os4.release()} ${os4.arch()}`));
88
661
  lines.push(info(`CPU: ${cpus2.length}x ${cpus2[0]?.model || "unknown"}`));
89
662
  lines.push(info(`RAM: ${bytesHuman(usedMem)} / ${bytesHuman(totalMem)} (${memPct})`));
90
- lines.push(info(`Uptime: ${msHuman(os.uptime() * 1e3)}`));
663
+ lines.push(info(`Uptime: ${msHuman(os4.uptime() * 1e3)}`));
91
664
  lines.push(info(`Node.js: ${process.version}`));
92
665
  check(
93
666
  totalMem >= 4 * 1024 * 1024 * 1024,
@@ -212,7 +785,7 @@ function diag_temp_default(pi) {
212
785
  }
213
786
  }
214
787
  lines.push(section("MODELS.JSON"));
215
- const agentDir = path.join(os.homedir(), ".pi", "agent");
788
+ const agentDir = path4.join(os4.homedir(), ".pi", "agent");
216
789
  let configuredModels = [];
217
790
  const modelsJson = readModelsJson();
218
791
  if (modelsJson && Object.keys(modelsJson.providers || {}).length > 0) {
@@ -273,12 +846,12 @@ function diag_temp_default(pi) {
273
846
  lines.push(fail(`settings.json read error: ${e.message}`));
274
847
  }
275
848
  lines.push(section("EXTENSIONS"));
276
- const extensionsDir = path.join(agentDir, "extensions");
849
+ const extensionsDir = path4.join(agentDir, "extensions");
277
850
  const activeTools = pi.getActiveTools();
278
851
  const allTools = pi.getAllTools();
279
852
  const builtinTools = /* @__PURE__ */ new Set(["read", "bash", "edit", "write"]);
280
853
  const extensionToolCount = activeTools.filter((t) => !builtinTools.has(t)).length;
281
- const localExtFiles = fs.existsSync(extensionsDir) ? fs.readdirSync(extensionsDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")) : [];
854
+ const localExtFiles = fs4.existsSync(extensionsDir) ? fs4.readdirSync(extensionsDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")) : [];
282
855
  lines.push(info(`Extension files in ${extensionsDir}: ${localExtFiles.length}`));
283
856
  localExtFiles.forEach((f) => lines.push(info(` \u2022 ${f}`)));
284
857
  if (localExtFiles.length > 0) {
@@ -295,15 +868,15 @@ function diag_temp_default(pi) {
295
868
  }
296
869
  lines.push(info(`Registered tools (all): ${allTools.length}`));
297
870
  lines.push(section("THEMES"));
298
- const themesDir = path.join(agentDir, "themes");
299
- if (fs.existsSync(themesDir)) {
300
- const themeFiles = fs.readdirSync(themesDir).filter(
871
+ const themesDir = path4.join(agentDir, "themes");
872
+ if (fs4.existsSync(themesDir)) {
873
+ const themeFiles = fs4.readdirSync(themesDir).filter(
301
874
  (f) => f.endsWith(".json")
302
875
  );
303
876
  lines.push(info(`Theme files: ${themeFiles.length}`));
304
877
  themeFiles.forEach((f) => {
305
878
  try {
306
- const theme = JSON.parse(fs.readFileSync(path.join(themesDir, f), "utf-8"));
879
+ const theme = JSON.parse(fs4.readFileSync(path4.join(themesDir, f), "utf-8"));
307
880
  lines.push(info(` \u2022 ${f} (name: "${theme.name || "unnamed"}")`));
308
881
  } catch {
309
882
  lines.push(warn(` \u2022 ${f} \u2014 INVALID JSON`));
@@ -402,7 +975,7 @@ function diag_temp_default(pi) {
402
975
  }
403
976
  lines.push(info("Audit log status:"));
404
977
  const auditEntries = readRecentAuditEntries(50);
405
- if (fs.existsSync(AUDIT_LOG_PATH)) {
978
+ if (fs4.existsSync(AUDIT_LOG_PATH)) {
406
979
  lines.push(ok(`Audit log exists: ${AUDIT_LOG_PATH}`));
407
980
  if (auditEntries.length > 0) {
408
981
  lines.push(info(` Recent entries: ${auditEntries.length} (last 50)`));
@@ -577,5 +1150,5 @@ function diag_temp_default(pi) {
577
1150
  });
578
1151
  }
579
1152
  export {
580
- diag_temp_default as default
1153
+ diag_default as default
581
1154
  };
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@vtstech/pi-diag",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Diagnostics extension for Pi Coding Agent",
5
5
  "main": "diag.js",
6
- "keywords": ["pi-package", "pi", "pi-coding-agent", "pi-extensions"],
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-coding-agent",
10
+ "pi-extensions"
11
+ ],
7
12
  "license": "MIT",
8
13
  "access": "public",
9
14
  "type": "module",
@@ -13,13 +18,12 @@
13
18
  "type": "git",
14
19
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
20
  },
16
- "dependencies": {
17
- "@vtstech/pi-shared": "1.2.1"
18
- },
19
21
  "peerDependencies": {
20
22
  "@mariozechner/pi-coding-agent": ">=0.66"
21
23
  },
22
24
  "pi": {
23
- "extensions": ["./diag.js"]
25
+ "extensions": [
26
+ "./diag.js"
27
+ ]
24
28
  }
25
29
  }