@vtstech/pi-diag 1.0.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.
- package/diag.js +512 -0
- package/package.json +24 -0
package/diag.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// .build-npm/diag/diag.temp.ts
|
|
30
|
+
var diag_temp_exports = {};
|
|
31
|
+
__export(diag_temp_exports, {
|
|
32
|
+
default: () => diag_temp_default
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(diag_temp_exports);
|
|
35
|
+
var fs = __toESM(require("node:fs"));
|
|
36
|
+
var os = __toESM(require("node:os"));
|
|
37
|
+
var path = __toESM(require("node:path"));
|
|
38
|
+
var import_format = require("@vtstech/pi-shared/format");
|
|
39
|
+
var import_ollama = require("@vtstech/pi-shared/ollama");
|
|
40
|
+
var import_security = require("@vtstech/pi-shared/security");
|
|
41
|
+
function diag_temp_default(pi) {
|
|
42
|
+
const branding = [
|
|
43
|
+
` \u26A1 Pi Diagnostics v1.0.3`,
|
|
44
|
+
` Written by VTSTech`,
|
|
45
|
+
` GitHub: https://github.com/VTSTech`,
|
|
46
|
+
` Website: www.vts-tech.org`
|
|
47
|
+
].join("\n");
|
|
48
|
+
async function runDiagnostics(ctx) {
|
|
49
|
+
const lines = [];
|
|
50
|
+
let passCount = 0;
|
|
51
|
+
let failCount = 0;
|
|
52
|
+
let warnCount = 0;
|
|
53
|
+
lines.push(branding);
|
|
54
|
+
const check = (condition, passMsg, failMsg) => {
|
|
55
|
+
if (condition) {
|
|
56
|
+
lines.push((0, import_format.ok)(passMsg));
|
|
57
|
+
passCount++;
|
|
58
|
+
} else {
|
|
59
|
+
lines.push((0, import_format.fail)(failMsg));
|
|
60
|
+
failCount++;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const warning = (condition, msg) => {
|
|
64
|
+
if (condition) {
|
|
65
|
+
lines.push((0, import_format.warn)(msg));
|
|
66
|
+
warnCount++;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
lines.push((0, import_format.section)("SYSTEM"));
|
|
70
|
+
const cpus2 = os.cpus();
|
|
71
|
+
const totalMem = os.totalmem();
|
|
72
|
+
const freeMem = os.freemem();
|
|
73
|
+
const usedMem = totalMem - freeMem;
|
|
74
|
+
const memPct = (0, import_format.pct)(usedMem, totalMem);
|
|
75
|
+
lines.push((0, import_format.info)(`OS: ${os.type()} ${os.release()} ${os.arch()}`));
|
|
76
|
+
lines.push((0, import_format.info)(`CPU: ${cpus2.length}x ${cpus2[0]?.model || "unknown"}`));
|
|
77
|
+
lines.push((0, import_format.info)(`RAM: ${(0, import_format.bytesHuman)(usedMem)} / ${(0, import_format.bytesHuman)(totalMem)} (${memPct})`));
|
|
78
|
+
lines.push((0, import_format.info)(`Uptime: ${(0, import_format.msHuman)(os.uptime() * 1e3)}`));
|
|
79
|
+
lines.push((0, import_format.info)(`Node.js: ${process.version}`));
|
|
80
|
+
check(
|
|
81
|
+
totalMem >= 4 * 1024 * 1024 * 1024,
|
|
82
|
+
`Total RAM: ${(0, import_format.bytesHuman)(totalMem)} (\u22654GB)`,
|
|
83
|
+
`Total RAM: ${(0, import_format.bytesHuman)(totalMem)} \u2014 LOW (<4GB), may struggle with models`
|
|
84
|
+
);
|
|
85
|
+
warning(
|
|
86
|
+
totalMem > 0 && usedMem / totalMem > 0.85,
|
|
87
|
+
`RAM usage ${memPct} \u2014 HIGH, close apps or reduce model size`
|
|
88
|
+
);
|
|
89
|
+
warning(cpus2.length < 2, `Only ${cpus2.length} CPU core(s), inference will be slow`);
|
|
90
|
+
lines.push((0, import_format.section)("DISK"));
|
|
91
|
+
try {
|
|
92
|
+
const dfResult = await pi.exec("df", ["-h", "/"], { timeout: 5e3 });
|
|
93
|
+
if (dfResult.code === 0) {
|
|
94
|
+
const dfLines = dfResult.stdout.trim().split("\n");
|
|
95
|
+
if (dfLines.length > 1) {
|
|
96
|
+
const parts = dfLines[1].trim().split(/\s+/);
|
|
97
|
+
lines.push((0, import_format.info)(`Mount: ${parts[0] || "/"}`));
|
|
98
|
+
lines.push((0, import_format.info)(`Size: ${parts[1]}, Used: ${parts[2]}, Avail: ${parts[3]}, Use%: ${parts[4]}`));
|
|
99
|
+
const usePct = parseInt(parts[4]) || 0;
|
|
100
|
+
warning(usePct > 90, `Disk usage ${parts[4]} \u2014 LOW SPACE`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
lines.push((0, import_format.warn)("Could not read disk info"));
|
|
105
|
+
}
|
|
106
|
+
lines.push((0, import_format.section)("OLLAMA"));
|
|
107
|
+
let ollamaOk = false;
|
|
108
|
+
let ollamaModels = [];
|
|
109
|
+
let ollamaVersion = "unknown";
|
|
110
|
+
const ollamaBaseUrl = (0, import_ollama.getOllamaBaseUrl)();
|
|
111
|
+
const isRemoteOllama = !ollamaBaseUrl.includes("localhost") && !ollamaBaseUrl.includes("127.0.0.1");
|
|
112
|
+
if (isRemoteOllama) {
|
|
113
|
+
const ollamaRoot = ollamaBaseUrl.replace(/\/v1\/?$/, "");
|
|
114
|
+
lines.push((0, import_format.info)(`Remote Ollama detected: ${ollamaBaseUrl}`));
|
|
115
|
+
try {
|
|
116
|
+
const startTime = Date.now();
|
|
117
|
+
const versionRes = await fetch(`${ollamaRoot}/api/version`, { signal: AbortSignal.timeout(1e4) });
|
|
118
|
+
const latency = Date.now() - startTime;
|
|
119
|
+
if (versionRes.ok) {
|
|
120
|
+
const versionData = await versionRes.json();
|
|
121
|
+
ollamaVersion = versionData.version || "unknown";
|
|
122
|
+
ollamaOk = true;
|
|
123
|
+
lines.push((0, import_format.ok)(`Remote Ollama running: ${ollamaVersion} (${(0, import_format.msHuman)(latency)} response time)`));
|
|
124
|
+
} else {
|
|
125
|
+
lines.push((0, import_format.fail)(`Remote Ollama returned status ${versionRes.status}`));
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
lines.push((0, import_format.fail)(`Remote Ollama not reachable: ${e.message || "unknown error"}`));
|
|
129
|
+
}
|
|
130
|
+
if (ollamaOk) {
|
|
131
|
+
try {
|
|
132
|
+
const tagsRes = await fetch(`${ollamaRoot}/api/tags`, { signal: AbortSignal.timeout(15e3) });
|
|
133
|
+
if (tagsRes.ok) {
|
|
134
|
+
const tagsData = await tagsRes.json();
|
|
135
|
+
ollamaModels = (tagsData.models || []).map((m) => m.name || m.model).filter(Boolean);
|
|
136
|
+
lines.push((0, import_format.info)(`Available models: ${ollamaModels.length}`));
|
|
137
|
+
ollamaModels.forEach((m) => lines.push((0, import_format.info)(` \u2022 ${m}`)));
|
|
138
|
+
check(ollamaModels.length > 0, "Models found in Ollama", "No models pulled in Ollama");
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
lines.push((0, import_format.warn)("Could not list remote Ollama models"));
|
|
142
|
+
}
|
|
143
|
+
try {
|
|
144
|
+
const psRes = await fetch(`${ollamaRoot}/api/ps`, { signal: AbortSignal.timeout(1e4) });
|
|
145
|
+
if (psRes.ok) {
|
|
146
|
+
const psData = await psRes.json();
|
|
147
|
+
const loaded = psData.models || [];
|
|
148
|
+
if (loaded.length > 0) {
|
|
149
|
+
lines.push((0, import_format.info)(`Loaded in VRAM: ${loaded[0].name || loaded[0].model || "unknown"}`));
|
|
150
|
+
} else {
|
|
151
|
+
lines.push((0, import_format.info)("No model currently loaded in Ollama"));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} catch {
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
try {
|
|
159
|
+
const startTime = Date.now();
|
|
160
|
+
const versionResult = await pi.exec("ollama", ["--version"], { timeout: 1e4 });
|
|
161
|
+
const latency = Date.now() - startTime;
|
|
162
|
+
if (versionResult.code === 0) {
|
|
163
|
+
ollamaVersion = versionResult.stdout.trim();
|
|
164
|
+
ollamaOk = true;
|
|
165
|
+
lines.push((0, import_format.ok)(`Ollama running: ${ollamaVersion} (${(0, import_format.msHuman)(latency)} response time)`));
|
|
166
|
+
} else {
|
|
167
|
+
lines.push((0, import_format.fail)(`Ollama error: ${versionResult.stderr.trim() || "non-zero exit code"}`));
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
lines.push((0, import_format.fail)(`Ollama not reachable: ${e.message || "unknown error"}`));
|
|
171
|
+
}
|
|
172
|
+
if (ollamaOk) {
|
|
173
|
+
try {
|
|
174
|
+
const listResult = await pi.exec("ollama", ["list"], { timeout: 15e3 });
|
|
175
|
+
if (listResult.code === 0) {
|
|
176
|
+
const modelLines = listResult.stdout.trim().split("\n").slice(1);
|
|
177
|
+
ollamaModels = modelLines.map((l) => l.trim().split(/\s+/)[0]).filter(Boolean);
|
|
178
|
+
lines.push((0, import_format.info)(`Available models: ${ollamaModels.length}`));
|
|
179
|
+
ollamaModels.forEach((m) => lines.push((0, import_format.info)(` \u2022 ${m}`)));
|
|
180
|
+
check(ollamaModels.length > 0, "Models found in Ollama", "No models pulled in Ollama");
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
lines.push((0, import_format.warn)("Could not list Ollama models"));
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const psResult = await pi.exec("ollama", ["ps"], { timeout: 1e4 });
|
|
187
|
+
if (psResult.code === 0) {
|
|
188
|
+
const psLines = psResult.stdout.trim().split("\n").slice(1);
|
|
189
|
+
if (psLines.length > 0) {
|
|
190
|
+
const loadedModel = psLines[0].trim().split(/\s+/)[0];
|
|
191
|
+
lines.push((0, import_format.info)(`Loaded in VRAM: ${loadedModel}`));
|
|
192
|
+
} else {
|
|
193
|
+
lines.push((0, import_format.warn)("No model currently loaded in Ollama"));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
lines.push((0, import_format.section)("MODELS.JSON"));
|
|
201
|
+
const agentDir = path.join(os.homedir(), ".pi", "agent");
|
|
202
|
+
const modelsJsonPath = import_ollama.MODELS_JSON_PATH;
|
|
203
|
+
let configuredModels = [];
|
|
204
|
+
let modelsJson = null;
|
|
205
|
+
if (fs.existsSync(modelsJsonPath)) {
|
|
206
|
+
try {
|
|
207
|
+
modelsJson = JSON.parse(fs.readFileSync(modelsJsonPath, "utf-8"));
|
|
208
|
+
const providers = modelsJson.providers || {};
|
|
209
|
+
lines.push((0, import_format.info)(`Providers configured: ${Object.keys(providers).length}`));
|
|
210
|
+
for (const [providerName, providerConfig] of Object.entries(providers)) {
|
|
211
|
+
const cfg = providerConfig;
|
|
212
|
+
const models = cfg.models || [];
|
|
213
|
+
lines.push((0, import_format.info)(` ${providerName}: ${cfg.baseUrl || "no baseUrl"}, ${models.length} models`));
|
|
214
|
+
for (const m of models) {
|
|
215
|
+
configuredModels.push(m.id);
|
|
216
|
+
const reasoning = m.reasoning ? " [reasoning]" : "";
|
|
217
|
+
lines.push((0, import_format.info)(` \u2022 ${m.id}${reasoning}`));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
check(
|
|
221
|
+
configuredModels.length > 0,
|
|
222
|
+
`${configuredModels.length} model(s) configured`,
|
|
223
|
+
"No models in models.json"
|
|
224
|
+
);
|
|
225
|
+
if (ollamaModels.length > 0) {
|
|
226
|
+
const missing = ollamaModels.filter((m) => !configuredModels.includes(m));
|
|
227
|
+
const extra = configuredModels.filter((m) => !ollamaModels.includes(m));
|
|
228
|
+
if (missing.length > 0) {
|
|
229
|
+
lines.push((0, import_format.warn)(`${missing.length} Ollama model(s) not in models.json: ${missing.join(", ")}`));
|
|
230
|
+
lines.push((0, import_format.info)(" \u2192 Run /ollama-sync to auto-sync"));
|
|
231
|
+
}
|
|
232
|
+
if (extra.length > 0) {
|
|
233
|
+
lines.push((0, import_format.warn)(`${extra.length} model(s) in models.json but not pulled in Ollama: ${extra.join(", ")}`));
|
|
234
|
+
}
|
|
235
|
+
if (missing.length === 0 && extra.length === 0) {
|
|
236
|
+
lines.push((0, import_format.ok)("models.json matches Ollama exactly"));
|
|
237
|
+
passCount++;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
lines.push((0, import_format.fail)(`models.json parse error: ${e.message}`));
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
lines.push((0, import_format.fail)(`models.json not found at ${modelsJsonPath}`));
|
|
245
|
+
lines.push((0, import_format.info)(" \u2192 Run /ollama-sync to create it"));
|
|
246
|
+
}
|
|
247
|
+
lines.push((0, import_format.section)("SETTINGS"));
|
|
248
|
+
const settingsPath = path.join(agentDir, "settings.json");
|
|
249
|
+
if (fs.existsSync(settingsPath)) {
|
|
250
|
+
try {
|
|
251
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
252
|
+
lines.push((0, import_format.info)("Global settings found:"));
|
|
253
|
+
for (const [key, val] of Object.entries(settings)) {
|
|
254
|
+
lines.push((0, import_format.info)(` ${key}: ${JSON.stringify(val)}`));
|
|
255
|
+
}
|
|
256
|
+
check(true, "settings.json valid JSON", "");
|
|
257
|
+
} catch (e) {
|
|
258
|
+
lines.push((0, import_format.fail)(`settings.json parse error: ${e.message}`));
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
lines.push((0, import_format.warn)("No global settings.json found (using defaults)"));
|
|
262
|
+
}
|
|
263
|
+
lines.push((0, import_format.section)("EXTENSIONS"));
|
|
264
|
+
const extensionsDir = path.join(agentDir, "extensions");
|
|
265
|
+
const activeTools = pi.getActiveTools();
|
|
266
|
+
const allTools = pi.getAllTools();
|
|
267
|
+
const builtinTools = /* @__PURE__ */ new Set(["read", "bash", "edit", "write"]);
|
|
268
|
+
const extensionToolCount = activeTools.filter((t) => !builtinTools.has(t)).length;
|
|
269
|
+
const localExtFiles = fs.existsSync(extensionsDir) ? fs.readdirSync(extensionsDir).filter((f) => f.endsWith(".ts") || f.endsWith(".js")) : [];
|
|
270
|
+
lines.push((0, import_format.info)(`Extension files in ${extensionsDir}: ${localExtFiles.length}`));
|
|
271
|
+
localExtFiles.forEach((f) => lines.push((0, import_format.info)(` \u2022 ${f}`)));
|
|
272
|
+
if (localExtFiles.length > 0) {
|
|
273
|
+
check(true, `${localExtFiles.length} local extension(s) found`);
|
|
274
|
+
} else if (extensionToolCount > 0) {
|
|
275
|
+
lines.push((0, import_format.info)(`${extensionToolCount} extension tool(s) loaded from Pi package`));
|
|
276
|
+
check(true, `${extensionToolCount} extension(s) active via Pi package`);
|
|
277
|
+
} else {
|
|
278
|
+
check(false, "", "No extensions found");
|
|
279
|
+
}
|
|
280
|
+
lines.push((0, import_format.info)(`Active tools: ${activeTools.length}`));
|
|
281
|
+
if (activeTools.length > 0) {
|
|
282
|
+
activeTools.forEach((t) => lines.push((0, import_format.info)(` \u2022 ${t}`)));
|
|
283
|
+
}
|
|
284
|
+
lines.push((0, import_format.info)(`Registered tools (all): ${allTools.length}`));
|
|
285
|
+
lines.push((0, import_format.section)("THEMES"));
|
|
286
|
+
const themesDir = path.join(agentDir, "themes");
|
|
287
|
+
if (fs.existsSync(themesDir)) {
|
|
288
|
+
const themeFiles = fs.readdirSync(themesDir).filter(
|
|
289
|
+
(f) => f.endsWith(".json")
|
|
290
|
+
);
|
|
291
|
+
lines.push((0, import_format.info)(`Theme files: ${themeFiles.length}`));
|
|
292
|
+
themeFiles.forEach((f) => {
|
|
293
|
+
try {
|
|
294
|
+
const theme = JSON.parse(fs.readFileSync(path.join(themesDir, f), "utf-8"));
|
|
295
|
+
lines.push((0, import_format.info)(` \u2022 ${f} (name: "${theme.name || "unnamed"}")`));
|
|
296
|
+
} catch {
|
|
297
|
+
lines.push((0, import_format.warn)(` \u2022 ${f} \u2014 INVALID JSON`));
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
} else {
|
|
301
|
+
lines.push((0, import_format.warn)(`Themes directory not found: ${themesDir}`));
|
|
302
|
+
}
|
|
303
|
+
lines.push((0, import_format.section)("SECURITY"));
|
|
304
|
+
const blockedCmdList = Array.from(import_security.BLOCKED_COMMANDS).sort();
|
|
305
|
+
lines.push((0, import_format.info)(`Command blocklist: ${blockedCmdList.length} commands blocked`));
|
|
306
|
+
const exampleCmds = blockedCmdList.filter((c) => ["rm", "sudo", "chmod", "curl", "wget", "eval"].includes(c));
|
|
307
|
+
if (exampleCmds.length > 0) {
|
|
308
|
+
lines.push((0, import_format.info)(` Examples: ${exampleCmds.join(", ")}`));
|
|
309
|
+
}
|
|
310
|
+
check(
|
|
311
|
+
blockedCmdList.length > 0,
|
|
312
|
+
`Command blocklist active (${blockedCmdList.length} rules)`,
|
|
313
|
+
`Command blocklist is EMPTY \u2014 security risk!`
|
|
314
|
+
);
|
|
315
|
+
const blockedPatterns = Array.from(import_security.BLOCKED_URL_PATTERNS).sort();
|
|
316
|
+
lines.push((0, import_format.info)(`SSRF protection: ${blockedPatterns.length} hostname patterns blocked`));
|
|
317
|
+
const examplePatterns = blockedPatterns.filter(
|
|
318
|
+
(p) => ["localhost", "127.0.0.1", "169.254.169.254", "10.", "192.168.", "internal."].includes(p)
|
|
319
|
+
);
|
|
320
|
+
if (examplePatterns.length > 0) {
|
|
321
|
+
lines.push((0, import_format.info)(` Examples: ${examplePatterns.join(", ")}`));
|
|
322
|
+
}
|
|
323
|
+
check(
|
|
324
|
+
blockedPatterns.length > 0,
|
|
325
|
+
`SSRF protection active (${blockedPatterns.length} patterns)`,
|
|
326
|
+
`SSRF blocklist is EMPTY \u2014 vulnerability risk!`
|
|
327
|
+
);
|
|
328
|
+
lines.push((0, import_format.info)("SSRF validation tests:"));
|
|
329
|
+
const ssrfTests = [
|
|
330
|
+
{ url: "http://localhost:8080/api", expectBlocked: true },
|
|
331
|
+
{ url: "http://169.254.169.254/latest/meta-data/", expectBlocked: true },
|
|
332
|
+
{ url: "http://192.168.1.1/admin", expectBlocked: true },
|
|
333
|
+
{ url: "https://api.example.com/data", expectBlocked: false }
|
|
334
|
+
];
|
|
335
|
+
for (const test of ssrfTests) {
|
|
336
|
+
const result = (0, import_security.isSafeUrl)(test.url);
|
|
337
|
+
if (test.expectBlocked && !result.safe) {
|
|
338
|
+
lines.push((0, import_format.ok)(` BLOCKED: ${test.url} \u2192 ${result.error}`));
|
|
339
|
+
} else if (!test.expectBlocked && result.safe) {
|
|
340
|
+
lines.push((0, import_format.ok)(` ALLOWED: ${test.url}`));
|
|
341
|
+
} else {
|
|
342
|
+
lines.push((0, import_format.fail)(` UNEXPECTED: ${test.url} \u2192 safe=${result.safe} (expected blocked=${test.expectBlocked})`));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
lines.push((0, import_format.info)("Path validation tests:"));
|
|
346
|
+
const pathTests = [
|
|
347
|
+
{ p: "/etc/passwd", expectValid: false },
|
|
348
|
+
{ p: "/etc/shadow", expectValid: false },
|
|
349
|
+
{ p: "../../etc/hosts", expectValid: false },
|
|
350
|
+
{ p: "./test.txt", expectValid: true },
|
|
351
|
+
{ p: "/tmp/output.log", expectValid: true },
|
|
352
|
+
{ p: process.cwd(), expectValid: true }
|
|
353
|
+
];
|
|
354
|
+
for (const test of pathTests) {
|
|
355
|
+
const result = (0, import_security.validatePath)(test.p);
|
|
356
|
+
if (result.valid === test.expectValid) {
|
|
357
|
+
if (test.expectValid) {
|
|
358
|
+
lines.push((0, import_format.ok)(` ALLOWED: ${test.p}`));
|
|
359
|
+
} else {
|
|
360
|
+
lines.push((0, import_format.ok)(` BLOCKED: ${test.p} \u2192 ${result.error}`));
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
lines.push((0, import_format.fail)(` UNEXPECTED: ${test.p} \u2192 valid=${result.valid} (expected valid=${test.expectValid})`));
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
lines.push((0, import_format.info)("Command injection tests:"));
|
|
367
|
+
const cmdTests = [
|
|
368
|
+
{ cmd: "ls; rm -rf /", expectSafe: false },
|
|
369
|
+
{ cmd: "sudo chmod 777 /etc/passwd", expectSafe: false },
|
|
370
|
+
{ cmd: "curl http://localhost/secret", expectSafe: false },
|
|
371
|
+
{ cmd: "ls -la", expectSafe: true },
|
|
372
|
+
{ cmd: "cat README.md", expectSafe: true },
|
|
373
|
+
{ cmd: "echo hello", expectSafe: true }
|
|
374
|
+
];
|
|
375
|
+
for (const test of cmdTests) {
|
|
376
|
+
const result = (0, import_security.sanitizeCommand)(test.cmd);
|
|
377
|
+
if (result.isSafe === test.expectSafe) {
|
|
378
|
+
if (test.expectSafe) {
|
|
379
|
+
lines.push((0, import_format.ok)(` PASS: "${test.cmd}" \u2192 allowed`));
|
|
380
|
+
} else {
|
|
381
|
+
lines.push((0, import_format.ok)(` BLOCKED: "${test.cmd}" \u2192 ${result.error}`));
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
lines.push((0, import_format.fail)(` UNEXPECTED: "${test.cmd}" \u2192 safe=${result.isSafe} (expected safe=${test.expectSafe})`));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
lines.push((0, import_format.info)("Audit log status:"));
|
|
388
|
+
const auditEntries = (0, import_security.readRecentAuditEntries)(50);
|
|
389
|
+
const auditLogPath = path.join(os.homedir(), ".pi", "agent", "audit.log");
|
|
390
|
+
if (fs.existsSync(auditLogPath)) {
|
|
391
|
+
lines.push((0, import_format.ok)(`Audit log exists: ${auditLogPath}`));
|
|
392
|
+
if (auditEntries.length > 0) {
|
|
393
|
+
lines.push((0, import_format.info)(` Recent entries: ${auditEntries.length} (last 50)`));
|
|
394
|
+
const recentSample = auditEntries.slice(-3);
|
|
395
|
+
for (const entry of recentSample) {
|
|
396
|
+
const entryType = (entry.type ?? entry.action ?? entry.event ?? "unknown").toString();
|
|
397
|
+
const entryTime = (entry.timestamp ?? entry.time ?? "").toString();
|
|
398
|
+
lines.push((0, import_format.info)(` \u2022 [${entryTime ? entryTime + "] " : ""}${entryType}`));
|
|
399
|
+
}
|
|
400
|
+
} else {
|
|
401
|
+
lines.push((0, import_format.info)(" No audit entries found (log is empty or unparseable)"));
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
lines.push((0, import_format.warn)(`Audit log not found at ${auditLogPath}`));
|
|
405
|
+
lines.push((0, import_format.info)(" \u2192 Audit logging will begin when security events occur"));
|
|
406
|
+
}
|
|
407
|
+
lines.push((0, import_format.section)("CURRENT SESSION"));
|
|
408
|
+
const model = ctx.model;
|
|
409
|
+
if (model) {
|
|
410
|
+
lines.push((0, import_format.info)(`Model: ${model.id || "unknown"}`));
|
|
411
|
+
lines.push((0, import_format.info)(`Provider: ${model.provider || "unknown"}`));
|
|
412
|
+
const BUILTIN_PROVIDERS = {
|
|
413
|
+
openrouter: { api: "openai-completions", baseUrl: "https://openrouter.ai/api/v1" },
|
|
414
|
+
anthropic: { api: "anthropic-messages", baseUrl: "https://api.anthropic.com" },
|
|
415
|
+
google: { api: "gemini", baseUrl: "https://generativelanguage.googleapis.com" },
|
|
416
|
+
openai: { api: "openai-completions", baseUrl: "https://api.openai.com" },
|
|
417
|
+
groq: { api: "openai-completions", baseUrl: "https://api.groq.com" },
|
|
418
|
+
deepseek: { api: "openai-completions", baseUrl: "https://api.deepseek.com" },
|
|
419
|
+
mistral: { api: "openai-completions", baseUrl: "https://api.mistral.ai" },
|
|
420
|
+
xai: { api: "openai-completions", baseUrl: "https://api.x.ai" },
|
|
421
|
+
together: { api: "openai-completions", baseUrl: "https://api.together.xyz" },
|
|
422
|
+
fireworks: { api: "openai-completions", baseUrl: "https://api.fireworks.ai" },
|
|
423
|
+
cohere: { api: "cohere-chat", baseUrl: "https://api.cohere.com" }
|
|
424
|
+
};
|
|
425
|
+
const providerName = model.provider || "";
|
|
426
|
+
const userProviderCfg = modelsJson ? (modelsJson.providers || {})[providerName] : null;
|
|
427
|
+
if (userProviderCfg) {
|
|
428
|
+
const apiMode = userProviderCfg.api || "not set";
|
|
429
|
+
const baseUrl = userProviderCfg.baseUrl || "not set";
|
|
430
|
+
lines.push((0, import_format.info)(`API mode: ${apiMode} (models.json)`));
|
|
431
|
+
lines.push((0, import_format.info)(`Base URL: ${baseUrl}`));
|
|
432
|
+
if (userProviderCfg.apiKey) {
|
|
433
|
+
lines.push((0, import_format.info)(`API key: ****${String(userProviderCfg.apiKey).slice(-4)}`));
|
|
434
|
+
}
|
|
435
|
+
} else if (BUILTIN_PROVIDERS[providerName]) {
|
|
436
|
+
const builtin = BUILTIN_PROVIDERS[providerName];
|
|
437
|
+
lines.push((0, import_format.info)(`API mode: ${builtin.api} (built-in: ${providerName})`));
|
|
438
|
+
lines.push((0, import_format.info)(`Base URL: ${builtin.baseUrl}`));
|
|
439
|
+
} else if (providerName) {
|
|
440
|
+
lines.push((0, import_format.info)(`API mode: unknown \u2014 provider "${providerName}" not in models.json or built-in list`));
|
|
441
|
+
} else {
|
|
442
|
+
lines.push((0, import_format.info)(`API mode: unknown \u2014 no provider configured`));
|
|
443
|
+
}
|
|
444
|
+
lines.push((0, import_format.info)(`Context window: ${model.contextWindow ?? "unknown"}`));
|
|
445
|
+
lines.push((0, import_format.info)(`Max tokens: ${model.maxTokens ?? "unknown"}`));
|
|
446
|
+
} else {
|
|
447
|
+
lines.push((0, import_format.warn)("No model selected"));
|
|
448
|
+
}
|
|
449
|
+
const usage = ctx.getContextUsage?.();
|
|
450
|
+
if (usage && usage.contextWindow > 0) {
|
|
451
|
+
lines.push((0, import_format.info)(`Context: ${usage.tokens ?? "?"} / ${usage.contextWindow} tokens (${(usage.tokens / usage.contextWindow * 100).toFixed(1)}%)`));
|
|
452
|
+
}
|
|
453
|
+
const thinking = pi.getThinkingLevel();
|
|
454
|
+
lines.push((0, import_format.info)(`Thinking level: ${thinking}`));
|
|
455
|
+
lines.push((0, import_format.section)("SUMMARY"));
|
|
456
|
+
lines.push((0, import_format.info)(`Passed: ${passCount} Failed: ${failCount} Warnings: ${warnCount}`));
|
|
457
|
+
if (failCount === 0) {
|
|
458
|
+
lines.push((0, import_format.ok)("All critical checks passed! \u{1F389}"));
|
|
459
|
+
} else {
|
|
460
|
+
lines.push((0, import_format.fail)(`${failCount} check(s) failed \u2014 see above for details`));
|
|
461
|
+
}
|
|
462
|
+
if (warnCount > 0) {
|
|
463
|
+
lines.push((0, import_format.warn)(`${warnCount} warning(s) \u2014 non-critical but worth addressing`));
|
|
464
|
+
}
|
|
465
|
+
lines.push(branding);
|
|
466
|
+
return lines.join("\n");
|
|
467
|
+
}
|
|
468
|
+
pi.registerCommand("diag", {
|
|
469
|
+
description: "Run a full system diagnostic (Ollama, models, extensions, themes, resources, security)",
|
|
470
|
+
handler: async (_args, ctx) => {
|
|
471
|
+
if (!ctx.hasUI) {
|
|
472
|
+
ctx.ui.notify("Diagnostic requires TUI mode", "error");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
ctx.ui.notify("Running diagnostic...", "info");
|
|
476
|
+
try {
|
|
477
|
+
const report = await runDiagnostics(ctx);
|
|
478
|
+
pi.sendMessage({
|
|
479
|
+
customType: "diagnostic-report",
|
|
480
|
+
content: report,
|
|
481
|
+
display: { type: "content", content: report }
|
|
482
|
+
});
|
|
483
|
+
} catch (e) {
|
|
484
|
+
ctx.ui.notify(`Diagnostic failed: ${e.message}`, "error");
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
pi.registerTool({
|
|
489
|
+
name: "self_diagnostic",
|
|
490
|
+
label: "Self Diagnostic",
|
|
491
|
+
description: "Run a comprehensive diagnostic check on the Pi environment including system resources, Ollama status, model configuration, extensions, themes, security posture, and current session state. Use this whenever the user asks for a diagnostic, health check, or system status.",
|
|
492
|
+
promptSnippet: "self_diagnostic - run full system diagnostic check",
|
|
493
|
+
promptGuidelines: [
|
|
494
|
+
"When the user asks for a diagnostic, health check, or system test, call self_diagnostic."
|
|
495
|
+
],
|
|
496
|
+
parameters: {},
|
|
497
|
+
execute: async (_toolCallId, _params, _signal, _onUpdate, ctx) => {
|
|
498
|
+
try {
|
|
499
|
+
const report = await runDiagnostics(ctx);
|
|
500
|
+
return {
|
|
501
|
+
content: [{ type: "text", text: report }],
|
|
502
|
+
isError: false
|
|
503
|
+
};
|
|
504
|
+
} catch (e) {
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: "text", text: `Diagnostic failed: ${e.message}` }],
|
|
507
|
+
isError: true
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vtstech/pi-diag",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Diagnostics extension for Pi Coding Agent",
|
|
5
|
+
"main": "diag.js",
|
|
6
|
+
"keywords": ["pi-package", "pi-extensions"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"access": "public",
|
|
9
|
+
"author": "VTSTech",
|
|
10
|
+
"homepage": "https://www.vts-tech.org",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/VTSTech/pi-coding-agent"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@vtstech/pi-shared": "1.0.3"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"@mariozechner/pi-coding-agent": ">=0.66"
|
|
20
|
+
},
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": ["./diag.js"]
|
|
23
|
+
}
|
|
24
|
+
}
|