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