@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.
Files changed (3) hide show
  1. package/README.md +40 -0
  2. package/diag.js +139 -153
  3. 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
- 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");
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((0, import_format.ok)(passMsg));
39
+ lines.push(ok(passMsg));
57
40
  passCount++;
58
41
  } else {
59
- lines.push((0, import_format.fail)(failMsg));
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((0, import_format.warn)(msg));
48
+ lines.push(warn(msg));
66
49
  warnCount++;
67
50
  }
68
51
  };
69
- lines.push((0, import_format.section)("SYSTEM"));
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 = (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}`));
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: ${(0, import_format.bytesHuman)(totalMem)} (\u22654GB)`,
83
- `Total RAM: ${(0, import_format.bytesHuman)(totalMem)} \u2014 LOW (<4GB), may struggle with models`
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((0, import_format.section)("DISK"));
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((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]}`));
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((0, import_format.warn)("Could not read disk info"));
87
+ lines.push(warn("Could not read disk info"));
105
88
  }
106
- lines.push((0, import_format.section)("OLLAMA"));
89
+ lines.push(section("OLLAMA"));
107
90
  let ollamaOk = false;
108
91
  let ollamaModels = [];
109
92
  let ollamaVersion = "unknown";
110
- const ollamaBaseUrl = (0, import_ollama.getOllamaBaseUrl)();
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((0, import_format.info)(`Remote Ollama detected: ${ollamaBaseUrl}`));
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((0, import_format.ok)(`Remote Ollama running: ${ollamaVersion} (${(0, import_format.msHuman)(latency)} response time)`));
106
+ lines.push(ok(`Remote Ollama running: ${ollamaVersion} (${msHuman(latency)} response time)`));
124
107
  } else {
125
- lines.push((0, import_format.fail)(`Remote Ollama returned status ${versionRes.status}`));
108
+ lines.push(fail(`Remote Ollama returned status ${versionRes.status}`));
126
109
  }
127
110
  } catch (e) {
128
- lines.push((0, import_format.fail)(`Remote Ollama not reachable: ${e.message || "unknown error"}`));
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((0, import_format.info)(`Available models: ${ollamaModels.length}`));
137
- ollamaModels.forEach((m) => lines.push((0, import_format.info)(` \u2022 ${m}`)));
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((0, import_format.warn)("Could not list remote Ollama models"));
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((0, import_format.info)(`Loaded in VRAM: ${loaded[0].name || loaded[0].model || "unknown"}`));
132
+ lines.push(info(`Loaded in VRAM: ${loaded[0].name || loaded[0].model || "unknown"}`));
150
133
  } else {
151
- lines.push((0, import_format.info)("No model currently loaded in Ollama"));
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((0, import_format.ok)(`Ollama running: ${ollamaVersion} (${(0, import_format.msHuman)(latency)} response time)`));
148
+ lines.push(ok(`Ollama running: ${ollamaVersion} (${msHuman(latency)} response time)`));
166
149
  } else {
167
- lines.push((0, import_format.fail)(`Ollama error: ${versionResult.stderr.trim() || "non-zero exit code"}`));
150
+ lines.push(fail(`Ollama error: ${versionResult.stderr.trim() || "non-zero exit code"}`));
168
151
  }
169
152
  } catch (e) {
170
- lines.push((0, import_format.fail)(`Ollama not reachable: ${e.message || "unknown error"}`));
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((0, import_format.info)(`Available models: ${ollamaModels.length}`));
179
- ollamaModels.forEach((m) => lines.push((0, import_format.info)(` \u2022 ${m}`)));
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((0, import_format.warn)("Could not list Ollama models"));
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((0, import_format.info)(`Loaded in VRAM: ${loadedModel}`));
174
+ lines.push(info(`Loaded in VRAM: ${loadedModel}`));
192
175
  } else {
193
- lines.push((0, import_format.warn)("No model currently loaded in Ollama"));
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((0, import_format.section)("MODELS.JSON"));
183
+ lines.push(section("MODELS.JSON"));
201
184
  const agentDir = path.join(os.homedir(), ".pi", "agent");
202
- const modelsJsonPath = import_ollama.MODELS_JSON_PATH;
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((0, import_format.info)(`Providers configured: ${Object.keys(providers).length}`));
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((0, import_format.info)(` ${providerName}: ${cfg.baseUrl || "no baseUrl"}, ${models.length} models`));
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((0, import_format.info)(` \u2022 ${m.id}${reasoning}`));
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((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"));
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((0, import_format.warn)(`${extra.length} model(s) in models.json but not pulled in Ollama: ${extra.join(", ")}`));
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((0, import_format.ok)("models.json matches Ollama exactly"));
219
+ lines.push(ok("models.json matches Ollama exactly"));
237
220
  passCount++;
238
221
  }
239
222
  }
240
223
  } catch (e) {
241
- lines.push((0, import_format.fail)(`models.json parse error: ${e.message}`));
224
+ lines.push(fail(`models.json parse error: ${e.message}`));
242
225
  }
243
226
  } 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"));
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((0, import_format.section)("SETTINGS"));
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((0, import_format.info)("Global settings found:"));
235
+ lines.push(info("Global settings found:"));
253
236
  for (const [key, val] of Object.entries(settings)) {
254
- lines.push((0, import_format.info)(` ${key}: ${JSON.stringify(val)}`));
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((0, import_format.fail)(`settings.json parse error: ${e.message}`));
241
+ lines.push(fail(`settings.json parse error: ${e.message}`));
259
242
  }
260
243
  } else {
261
- lines.push((0, import_format.warn)("No global settings.json found (using defaults)"));
244
+ lines.push(warn("No global settings.json found (using defaults)"));
262
245
  }
263
- lines.push((0, import_format.section)("EXTENSIONS"));
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((0, import_format.info)(`Extension files in ${extensionsDir}: ${localExtFiles.length}`));
271
- localExtFiles.forEach((f) => lines.push((0, import_format.info)(` \u2022 ${f}`)));
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((0, import_format.info)(`${extensionToolCount} extension tool(s) loaded from Pi package`));
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((0, import_format.info)(`Active tools: ${activeTools.length}`));
263
+ lines.push(info(`Active tools: ${activeTools.length}`));
281
264
  if (activeTools.length > 0) {
282
- activeTools.forEach((t) => lines.push((0, import_format.info)(` \u2022 ${t}`)));
265
+ activeTools.forEach((t) => lines.push(info(` \u2022 ${t}`)));
283
266
  }
284
- lines.push((0, import_format.info)(`Registered tools (all): ${allTools.length}`));
285
- lines.push((0, import_format.section)("THEMES"));
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((0, import_format.info)(`Theme files: ${themeFiles.length}`));
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((0, import_format.info)(` \u2022 ${f} (name: "${theme.name || "unnamed"}")`));
278
+ lines.push(info(` \u2022 ${f} (name: "${theme.name || "unnamed"}")`));
296
279
  } catch {
297
- lines.push((0, import_format.warn)(` \u2022 ${f} \u2014 INVALID JSON`));
280
+ lines.push(warn(` \u2022 ${f} \u2014 INVALID JSON`));
298
281
  }
299
282
  });
300
283
  } else {
301
- lines.push((0, import_format.warn)(`Themes directory not found: ${themesDir}`));
284
+ lines.push(warn(`Themes directory not found: ${themesDir}`));
302
285
  }
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`));
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((0, import_format.info)(` Examples: ${exampleCmds.join(", ")}`));
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(import_security.BLOCKED_URL_PATTERNS).sort();
316
- lines.push((0, import_format.info)(`SSRF protection: ${blockedPatterns.length} hostname patterns blocked`));
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((0, import_format.info)(` Examples: ${examplePatterns.join(", ")}`));
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((0, import_format.info)("SSRF validation tests:"));
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 = (0, import_security.isSafeUrl)(test.url);
319
+ const result = isSafeUrl(test.url);
337
320
  if (test.expectBlocked && !result.safe) {
338
- lines.push((0, import_format.ok)(` BLOCKED: ${test.url} \u2192 ${result.error}`));
321
+ lines.push(ok(` BLOCKED: ${test.url} \u2192 ${result.error}`));
339
322
  } else if (!test.expectBlocked && result.safe) {
340
- lines.push((0, import_format.ok)(` ALLOWED: ${test.url}`));
323
+ lines.push(ok(` ALLOWED: ${test.url}`));
341
324
  } else {
342
- lines.push((0, import_format.fail)(` UNEXPECTED: ${test.url} \u2192 safe=${result.safe} (expected blocked=${test.expectBlocked})`));
325
+ lines.push(fail(` UNEXPECTED: ${test.url} \u2192 safe=${result.safe} (expected blocked=${test.expectBlocked})`));
343
326
  }
344
327
  }
345
- lines.push((0, import_format.info)("Path validation tests:"));
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 = (0, import_security.validatePath)(test.p);
338
+ const result = validatePath(test.p);
356
339
  if (result.valid === test.expectValid) {
357
340
  if (test.expectValid) {
358
- lines.push((0, import_format.ok)(` ALLOWED: ${test.p}`));
341
+ lines.push(ok(` ALLOWED: ${test.p}`));
359
342
  } else {
360
- lines.push((0, import_format.ok)(` BLOCKED: ${test.p} \u2192 ${result.error}`));
343
+ lines.push(ok(` BLOCKED: ${test.p} \u2192 ${result.error}`));
361
344
  }
362
345
  } else {
363
- lines.push((0, import_format.fail)(` UNEXPECTED: ${test.p} \u2192 valid=${result.valid} (expected valid=${test.expectValid})`));
346
+ lines.push(fail(` UNEXPECTED: ${test.p} \u2192 valid=${result.valid} (expected valid=${test.expectValid})`));
364
347
  }
365
348
  }
366
- lines.push((0, import_format.info)("Command injection tests:"));
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 = (0, import_security.sanitizeCommand)(test.cmd);
359
+ const result = sanitizeCommand(test.cmd);
377
360
  if (result.isSafe === test.expectSafe) {
378
361
  if (test.expectSafe) {
379
- lines.push((0, import_format.ok)(` PASS: "${test.cmd}" \u2192 allowed`));
362
+ lines.push(ok(` PASS: "${test.cmd}" \u2192 allowed`));
380
363
  } else {
381
- lines.push((0, import_format.ok)(` BLOCKED: "${test.cmd}" \u2192 ${result.error}`));
364
+ lines.push(ok(` BLOCKED: "${test.cmd}" \u2192 ${result.error}`));
382
365
  }
383
366
  } else {
384
- lines.push((0, import_format.fail)(` UNEXPECTED: "${test.cmd}" \u2192 safe=${result.isSafe} (expected safe=${test.expectSafe})`));
367
+ lines.push(fail(` UNEXPECTED: "${test.cmd}" \u2192 safe=${result.isSafe} (expected safe=${test.expectSafe})`));
385
368
  }
386
369
  }
387
- lines.push((0, import_format.info)("Audit log status:"));
388
- const auditEntries = (0, import_security.readRecentAuditEntries)(50);
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((0, import_format.ok)(`Audit log exists: ${auditLogPath}`));
374
+ lines.push(ok(`Audit log exists: ${auditLogPath}`));
392
375
  if (auditEntries.length > 0) {
393
- lines.push((0, import_format.info)(` Recent entries: ${auditEntries.length} (last 50)`));
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((0, import_format.info)(` \u2022 [${entryTime ? entryTime + "] " : ""}${entryType}`));
381
+ lines.push(info(` \u2022 [${entryTime ? entryTime + "] " : ""}${entryType}`));
399
382
  }
400
383
  } else {
401
- lines.push((0, import_format.info)(" No audit entries found (log is empty or unparseable)"));
384
+ lines.push(info(" No audit entries found (log is empty or unparseable)"));
402
385
  }
403
386
  } 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"));
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((0, import_format.section)("CURRENT SESSION"));
390
+ lines.push(section("CURRENT SESSION"));
408
391
  const model = ctx.model;
409
392
  if (model) {
410
- lines.push((0, import_format.info)(`Model: ${model.id || "unknown"}`));
411
- lines.push((0, import_format.info)(`Provider: ${model.provider || "unknown"}`));
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((0, import_format.info)(`API mode: ${apiMode} (models.json)`));
431
- lines.push((0, import_format.info)(`Base URL: ${baseUrl}`));
413
+ lines.push(info(`API mode: ${apiMode} (models.json)`));
414
+ lines.push(info(`Base URL: ${baseUrl}`));
432
415
  if (userProviderCfg.apiKey) {
433
- lines.push((0, import_format.info)(`API key: ****${String(userProviderCfg.apiKey).slice(-4)}`));
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((0, import_format.info)(`API mode: ${builtin.api} (built-in: ${providerName})`));
438
- lines.push((0, import_format.info)(`Base URL: ${builtin.baseUrl}`));
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((0, import_format.info)(`API mode: unknown \u2014 provider "${providerName}" not in models.json or built-in list`));
423
+ lines.push(info(`API mode: unknown \u2014 provider "${providerName}" not in models.json or built-in list`));
441
424
  } else {
442
- lines.push((0, import_format.info)(`API mode: unknown \u2014 no provider configured`));
425
+ lines.push(info(`API mode: unknown \u2014 no provider configured`));
443
426
  }
444
- lines.push((0, import_format.info)(`Context window: ${model.contextWindow ?? "unknown"}`));
445
- lines.push((0, import_format.info)(`Max tokens: ${model.maxTokens ?? "unknown"}`));
427
+ lines.push(info(`Context window: ${model.contextWindow ?? "unknown"}`));
428
+ lines.push(info(`Max tokens: ${model.maxTokens ?? "unknown"}`));
446
429
  } else {
447
- lines.push((0, import_format.warn)("No model selected"));
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((0, import_format.info)(`Context: ${usage.tokens ?? "?"} / ${usage.contextWindow} tokens (${(usage.tokens / usage.contextWindow * 100).toFixed(1)}%)`));
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((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}`));
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((0, import_format.ok)("All critical checks passed! \u{1F389}"));
441
+ lines.push(ok("All critical checks passed! \u{1F389}"));
459
442
  } else {
460
- lines.push((0, import_format.fail)(`${failCount} check(s) failed \u2014 see above for details`));
443
+ lines.push(fail(`${failCount} check(s) failed \u2014 see above for details`));
461
444
  }
462
445
  if (warnCount > 0) {
463
- lines.push((0, import_format.warn)(`${warnCount} warning(s) \u2014 non-critical but worth addressing`));
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",
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.3"
17
+ "@vtstech/pi-shared": "1.0.4-1"
17
18
  },
18
19
  "peerDependencies": {
19
20
  "@mariozechner/pi-coding-agent": ">=0.66"