@vtstech/pi-status 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +10 -6
  2. package/status.js +392 -13
package/package.json CHANGED
@@ -1,9 +1,14 @@
1
1
  {
2
2
  "name": "@vtstech/pi-status",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "System monitor / status bar extension for Pi Coding Agent",
5
5
  "main": "status.js",
6
- "keywords": ["pi-package", "pi", "pi-coding-agent", "pi-extensions"],
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-coding-agent",
10
+ "pi-extensions"
11
+ ],
7
12
  "license": "MIT",
8
13
  "access": "public",
9
14
  "type": "module",
@@ -13,13 +18,12 @@
13
18
  "type": "git",
14
19
  "url": "https://github.com/VTSTech/pi-coding-agent"
15
20
  },
16
- "dependencies": {
17
- "@vtstech/pi-shared": "1.2.2"
18
- },
19
21
  "peerDependencies": {
20
22
  "@mariozechner/pi-coding-agent": ">=0.66"
21
23
  },
22
24
  "pi": {
23
- "extensions": ["./status.js"]
25
+ "extensions": [
26
+ "./status.js"
27
+ ]
24
28
  }
25
29
  }
package/status.js CHANGED
@@ -1,16 +1,395 @@
1
- // .build-npm/status/status.temp.ts
2
- import * as fs from "node:fs";
1
+ // extensions/status.ts
2
+ import * as fs3 from "node:fs";
3
3
  import { exec } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
+ import os4 from "node:os";
6
+
7
+ // shared/ollama.ts
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
5
10
  import os from "node:os";
6
- import { getOllamaBaseUrl, fetchModelContextLength, readModelsJson, isLocalProvider } from "@vtstech/pi-shared/ollama";
7
- import { fmtBytes, fmtDur } from "@vtstech/pi-shared/format";
8
- import { debugLog } from "@vtstech/pi-shared/debug";
9
- import { getSecurityMode } from "@vtstech/pi-shared/security";
11
+
12
+ // shared/debug.ts
13
+ var DEBUG_ENABLED = process?.env?.PI_EXTENSIONS_DEBUG === "1";
14
+ function debugLog(module, message, ...args) {
15
+ if (!DEBUG_ENABLED) return;
16
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
17
+ console.debug(`[pi-ext:${module}] ${timestamp} ${message}`, ...args);
18
+ }
19
+
20
+ // shared/ollama.ts
21
+ var MODELS_JSON_PATH = path.join(os.homedir(), ".pi", "agent", "models.json");
22
+ var _modelsJsonCache = null;
23
+ var _ollamaBaseUrlCache = null;
24
+ var CACHE_TTL_MS = 2e3;
25
+ function getOllamaBaseUrl() {
26
+ const now = Date.now();
27
+ if (_ollamaBaseUrlCache && now - _ollamaBaseUrlCache.ts < CACHE_TTL_MS) return _ollamaBaseUrlCache.data;
28
+ try {
29
+ if (fs.existsSync(MODELS_JSON_PATH)) {
30
+ const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
31
+ const config = JSON.parse(raw);
32
+ const baseUrl = config?.providers?.["ollama"]?.baseUrl;
33
+ if (baseUrl) {
34
+ const result = baseUrl.replace(/\/v1\/?$/, "");
35
+ _ollamaBaseUrlCache = { data: result, ts: now };
36
+ return result;
37
+ }
38
+ }
39
+ } catch (err) {
40
+ debugLog("ollama", "failed to parse models.json for base URL", err);
41
+ }
42
+ if (process.env.OLLAMA_HOST) {
43
+ const result = `http://${process.env.OLLAMA_HOST.replace(/^https?:\/\//, "")}`;
44
+ _ollamaBaseUrlCache = { data: result, ts: now };
45
+ return result;
46
+ }
47
+ const fallback = "http://localhost:11434";
48
+ _ollamaBaseUrlCache = { data: fallback, ts: now };
49
+ return fallback;
50
+ }
51
+ function readModelsJson() {
52
+ const now = Date.now();
53
+ if (_modelsJsonCache && now - _modelsJsonCache.ts < CACHE_TTL_MS) return _modelsJsonCache.data;
54
+ try {
55
+ if (fs.existsSync(MODELS_JSON_PATH)) {
56
+ const raw = fs.readFileSync(MODELS_JSON_PATH, "utf-8");
57
+ const data = JSON.parse(raw);
58
+ _modelsJsonCache = { data, ts: now };
59
+ return data;
60
+ }
61
+ } catch (err) {
62
+ debugLog("ollama", "failed to read/parse models.json", err);
63
+ }
64
+ const empty = { providers: {} };
65
+ _modelsJsonCache = { data: empty, ts: now };
66
+ return empty;
67
+ }
68
+ var DEFAULT_RETRY_OPTIONS = {
69
+ maxRetries: 2,
70
+ baseDelayMs: 1e3,
71
+ maxDelayMs: 1e4,
72
+ retryOnTimeout: true,
73
+ retryOnConnectionError: true
74
+ };
75
+ function backoffDelay(attempt, baseDelayMs, maxDelayMs) {
76
+ const delay = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
77
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
78
+ return Math.max(0, Math.round(delay + jitter));
79
+ }
80
+ var RETRYABLE_ERROR_PATTERNS = [
81
+ "ECONNREFUSED",
82
+ "ECONNRESET",
83
+ "ENOTFOUND",
84
+ "ETIMEDOUT",
85
+ "fetch failed",
86
+ "network error",
87
+ "socket hang up",
88
+ "Empty response"
89
+ ];
90
+ function isRetryableError(error, opts) {
91
+ if (error instanceof Error) {
92
+ if (error.name === "AbortError" && opts.retryOnTimeout) return true;
93
+ const msg = error.message;
94
+ if (opts.retryOnConnectionError && RETRYABLE_ERROR_PATTERNS.some((p) => msg.includes(p))) {
95
+ return true;
96
+ }
97
+ }
98
+ return false;
99
+ }
100
+ async function withRetry(fn, options) {
101
+ const opts = { ...DEFAULT_RETRY_OPTIONS, ...options };
102
+ let lastError;
103
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
104
+ try {
105
+ return await fn();
106
+ } catch (error) {
107
+ lastError = error;
108
+ if (attempt < opts.maxRetries && isRetryableError(error, opts)) {
109
+ const delay = backoffDelay(attempt, opts.baseDelayMs, opts.maxDelayMs);
110
+ debugLog("ollama", `Retry ${attempt + 1}/${opts.maxRetries} after ${delay}ms: ${error instanceof Error ? error.message : String(error)}`);
111
+ await new Promise((r) => setTimeout(r, delay));
112
+ continue;
113
+ }
114
+ throw error;
115
+ }
116
+ }
117
+ throw lastError;
118
+ }
119
+ async function fetchModelContextLength(baseUrl, modelName) {
120
+ return withRetry(async () => {
121
+ try {
122
+ const res = await fetch(`${baseUrl}/api/show`, {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({ name: modelName }),
126
+ signal: AbortSignal.timeout(3e4)
127
+ });
128
+ if (!res.ok) return void 0;
129
+ const data = await res.json();
130
+ for (const key of Object.keys(data?.model_info ?? {})) {
131
+ if (key.endsWith(".context_length")) {
132
+ const val = data.model_info[key];
133
+ if (typeof val === "number") return val;
134
+ }
135
+ }
136
+ const numCtx = data?.model_info?.["num_ctx"];
137
+ if (typeof numCtx === "number") return numCtx;
138
+ } catch (err) {
139
+ debugLog("ollama", `failed to fetch context length for ${modelName}`, err);
140
+ return void 0;
141
+ }
142
+ return void 0;
143
+ });
144
+ }
145
+ function isLocalProvider(baseUrl, providerName) {
146
+ if (providerName === "ollama") return true;
147
+ const url = baseUrl || "";
148
+ return url.includes("localhost") || url.includes("127.0.0.1") || url.includes("0.0.0.0");
149
+ }
150
+
151
+ // shared/format.ts
152
+ function fmtBytes(b) {
153
+ if (b === 0) return "0B";
154
+ if (b < 1024) return `${b}B`;
155
+ if (b >= 1073741824) return `${(b / 1073741824).toFixed(1)}G`;
156
+ if (b >= 1048576) return `${(b / 1048576).toFixed(0)}M`;
157
+ return `${(b / 1024).toFixed(0)}K`;
158
+ }
159
+ function fmtDur(ms) {
160
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
161
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
162
+ return `${Math.floor(ms / 6e4)}m${Math.round(ms % 6e4 / 1e3)}s`;
163
+ }
164
+
165
+ // shared/security.ts
166
+ import * as fs2 from "node:fs";
167
+ import * as path3 from "node:path";
168
+ import os3 from "node:os";
169
+
170
+ // shared/config-io.ts
171
+ import * as path2 from "path";
172
+ import os2 from "os";
173
+ var PI_AGENT_DIR = path2.join(os2.homedir(), ".pi", "agent");
174
+ var SETTINGS_PATH = path2.join(PI_AGENT_DIR, "settings.json");
175
+ var SECURITY_PATH = path2.join(PI_AGENT_DIR, "security.json");
176
+ var REACT_MODE_PATH = path2.join(PI_AGENT_DIR, "react-mode.json");
177
+ var MODEL_TEST_CONFIG_PATH = path2.join(PI_AGENT_DIR, "model-test-config.json");
178
+
179
+ // shared/security.ts
180
+ var SECURITY_CONFIG_PATH = SECURITY_PATH;
181
+ var securityModeCache = null;
182
+ var securityModeCacheTime = 0;
183
+ var SECURITY_CACHE_DURATION_MS = 3e4;
184
+ function getSecurityMode() {
185
+ const now = Date.now();
186
+ if (securityModeCache && now - securityModeCacheTime < SECURITY_CACHE_DURATION_MS) {
187
+ return securityModeCache;
188
+ }
189
+ try {
190
+ if (!fs2.existsSync(SECURITY_CONFIG_PATH)) {
191
+ securityModeCache = "max";
192
+ securityModeCacheTime = now;
193
+ return "max";
194
+ }
195
+ const raw = fs2.readFileSync(SECURITY_CONFIG_PATH, "utf-8");
196
+ const config = JSON.parse(raw);
197
+ if (config.mode === "basic" || config.mode === "max" || config.mode === "off") {
198
+ securityModeCache = config.mode;
199
+ securityModeCacheTime = now;
200
+ return config.mode;
201
+ }
202
+ securityModeCache = "max";
203
+ securityModeCacheTime = now;
204
+ return "max";
205
+ } catch (err) {
206
+ debugLog("security", `failed to read security config at ${SECURITY_CONFIG_PATH}`, err);
207
+ securityModeCache = "max";
208
+ securityModeCacheTime = now;
209
+ return "max";
210
+ }
211
+ }
212
+ var CRITICAL_COMMANDS = /* @__PURE__ */ new Set([
213
+ // Filesystem destruction (irrecoverable)
214
+ "mkfs",
215
+ "dd",
216
+ "shred",
217
+ "wipe",
218
+ "srm",
219
+ "format",
220
+ "fdisk",
221
+ // Privilege escalation (non-sudo)
222
+ "su",
223
+ "doas",
224
+ "pkexec",
225
+ "gksudo",
226
+ "kdesu",
227
+ // Network attack tools
228
+ "nmap",
229
+ "nc",
230
+ "netcat",
231
+ "telnet",
232
+ // Remote access
233
+ "ssh",
234
+ "scp",
235
+ "sftp",
236
+ "rsync",
237
+ // Process killing
238
+ "kill",
239
+ "killall",
240
+ "pkill",
241
+ "xkill",
242
+ // User management
243
+ "useradd",
244
+ "userdel",
245
+ "usermod",
246
+ "passwd",
247
+ "adduser",
248
+ "deluser",
249
+ // Dangerous shell features
250
+ "exec",
251
+ "eval",
252
+ "source",
253
+ ".",
254
+ "alias",
255
+ // Filesystem control
256
+ "mount",
257
+ "umount",
258
+ "chattr",
259
+ "lsattr",
260
+ // Permission modification
261
+ "chown",
262
+ "chmod"
263
+ ]);
264
+ var EXTENDED_COMMANDS = /* @__PURE__ */ new Set([
265
+ // File deletion
266
+ "rm",
267
+ "rmdir",
268
+ "del",
269
+ // Privilege escalation
270
+ "sudo",
271
+ // Download tools
272
+ "wget",
273
+ "curl",
274
+ // Package management
275
+ "apt",
276
+ "apt-get",
277
+ "yum",
278
+ "dnf",
279
+ "pacman",
280
+ "pip",
281
+ "npm",
282
+ "yarn",
283
+ "cargo",
284
+ // System service control
285
+ "systemctl",
286
+ "service",
287
+ // Interactive editors (shell escape risk)
288
+ "vi",
289
+ "vim",
290
+ "nano",
291
+ "emacs",
292
+ "less",
293
+ "more",
294
+ "man",
295
+ // Version control
296
+ "git"
297
+ ]);
298
+ var BLOCKED_COMMANDS = /* @__PURE__ */ new Set([
299
+ ...CRITICAL_COMMANDS,
300
+ ...EXTENDED_COMMANDS
301
+ ]);
302
+ var BLOCKED_URL_ALWAYS = /* @__PURE__ */ new Set([
303
+ // Cloud metadata endpoints
304
+ "169.254.169.254",
305
+ // RFC1918 private ranges
306
+ "10.",
307
+ "192.168.",
308
+ "172.16.",
309
+ "172.17.",
310
+ "172.18.",
311
+ "172.19.",
312
+ "172.20.",
313
+ "172.21.",
314
+ "172.22.",
315
+ "172.23.",
316
+ "172.24.",
317
+ "172.25.",
318
+ "172.26.",
319
+ "172.27.",
320
+ "172.28.",
321
+ "172.29.",
322
+ "172.30.",
323
+ "172.31.",
324
+ // IPv6-mapped IPv4 cloud metadata (always blocked)
325
+ "::ffff:169.254.169.254",
326
+ // Internal service patterns
327
+ "internal.",
328
+ "private.",
329
+ "intranet."
330
+ ]);
331
+ var BLOCKED_URL_MAX_ONLY = /* @__PURE__ */ new Set([
332
+ // Loopback addresses (full 127.0.0.0/8 range)
333
+ "localhost",
334
+ "127.",
335
+ "0.0.0.0",
336
+ "::1",
337
+ "::ffff:127.0.0.1",
338
+ "::ffff:0.0.0.0",
339
+ // IPv6-mapped IPv4 private ranges (always blocked in max mode)
340
+ "::ffff:10.",
341
+ "::ffff:192.168.",
342
+ "::ffff:172.16.",
343
+ "::ffff:172.17.",
344
+ "::ffff:172.18.",
345
+ "::ffff:172.19.",
346
+ "::ffff:172.20.",
347
+ "::ffff:172.21.",
348
+ "::ffff:172.22.",
349
+ "::ffff:172.23.",
350
+ "::ffff:172.24.",
351
+ "::ffff:172.25.",
352
+ "::ffff:172.26.",
353
+ "::ffff:172.27.",
354
+ "::ffff:172.28.",
355
+ "::ffff:172.29.",
356
+ "::ffff:172.30.",
357
+ "::ffff:172.31.",
358
+ // Local/internal patterns
359
+ "local."
360
+ ]);
361
+ var BLOCKED_URL_PATTERNS = /* @__PURE__ */ new Set([
362
+ ...BLOCKED_URL_ALWAYS,
363
+ ...BLOCKED_URL_MAX_ONLY
364
+ ]);
365
+ var AUDIT_DIR = path3.join(os3.homedir(), ".pi", "agent");
366
+ var AUDIT_LOG_PATH = path3.join(AUDIT_DIR, "audit.log");
367
+ var _auditBuffer = [];
368
+ function flushAuditBuffer() {
369
+ if (_auditBuffer.length === 0) return;
370
+ try {
371
+ if (!fs2.existsSync(AUDIT_DIR)) {
372
+ fs2.mkdirSync(AUDIT_DIR, { recursive: true });
373
+ }
374
+ const batch = _auditBuffer.join("");
375
+ fs2.appendFileSync(AUDIT_LOG_PATH, batch, "utf-8");
376
+ } catch (err) {
377
+ debugLog("security", "audit buffer flush failure", err);
378
+ }
379
+ _auditBuffer = [];
380
+ }
381
+ process.on("exit", () => {
382
+ flushAuditBuffer();
383
+ });
384
+ process.on("SIGTERM", () => {
385
+ flushAuditBuffer();
386
+ });
387
+
388
+ // extensions/status.ts
10
389
  var execAsync = promisify(exec);
11
390
  var STATUS_UPDATE_INTERVAL_MS = 5e3;
12
391
  var TOOL_TIMER_INTERVAL_MS = 1e3;
13
- function status_temp_default(pi) {
392
+ function status_default(pi) {
14
393
  let lastResponseTime = null;
15
394
  let agentStartTime = null;
16
395
  let updateInterval = null;
@@ -38,7 +417,7 @@ function status_temp_default(pi) {
38
417
  let activeToolStart = 0;
39
418
  let blockedCount = 0;
40
419
  function getCpuSnapshot() {
41
- return os.cpus().map((c) => ({
420
+ return os4.cpus().map((c) => ({
42
421
  user: c.times.user,
43
422
  nice: c.times.nice,
44
423
  sys: c.times.sys,
@@ -46,7 +425,7 @@ function status_temp_default(pi) {
46
425
  }));
47
426
  }
48
427
  function getCpuUsage() {
49
- const cpus = os.cpus();
428
+ const cpus = os4.cpus();
50
429
  const n = cpus.length;
51
430
  let totalUsed = 0, totalDelta = 0;
52
431
  for (let i = 0; i < n; i++) {
@@ -64,8 +443,8 @@ function status_temp_default(pi) {
64
443
  return totalDelta > 0 ? totalUsed / totalDelta * 100 : 0;
65
444
  }
66
445
  function getMem() {
67
- const total = os.totalmem();
68
- const used = total - os.freemem();
446
+ const total = os4.totalmem();
447
+ const used = total - os4.freemem();
69
448
  return { used, total };
70
449
  }
71
450
  async function getSwap() {
@@ -74,7 +453,7 @@ function status_temp_default(pi) {
74
453
  return null;
75
454
  }
76
455
  try {
77
- const out = await fs.promises.readFile("/proc/meminfo", "utf-8");
456
+ const out = await fs3.promises.readFile("/proc/meminfo", "utf-8");
78
457
  const swapTotal2 = Number(out.match(/SwapTotal:\s+(\d+)/)?.[1]) * 1024;
79
458
  const swapFree = Number(out.match(/SwapFree:\s+(\d+)/)?.[1]) * 1024;
80
459
  if (swapTotal2 > 0) return { used: swapTotal2 - swapFree, total: swapTotal2 };
@@ -347,5 +726,5 @@ function status_temp_default(pi) {
347
726
  });
348
727
  }
349
728
  export {
350
- status_temp_default as default
729
+ status_default as default
351
730
  };