@pi-unipi/utility 0.1.1 → 0.2.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 +121 -21
- package/package.json +16 -7
- package/skills/utility/SKILL.md +70 -0
- package/src/analytics/collector.ts +293 -0
- package/src/cache/ttl-cache.ts +311 -0
- package/src/commands.ts +186 -0
- package/src/diagnostics/engine.ts +298 -0
- package/src/display/capabilities.ts +200 -0
- package/src/display/width.ts +226 -0
- package/src/index.ts +172 -0
- package/src/info-screen.ts +80 -0
- package/src/lifecycle/cleanup.ts +332 -0
- package/src/lifecycle/process.ts +162 -0
- package/src/tools/batch.ts +229 -0
- package/src/tools/env.ts +134 -0
- package/src/tui/settings-inspector.ts +303 -0
- package/src/types.ts +257 -0
- package/commands.ts +0 -38
- package/index.ts +0 -34
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — TTL Cache
|
|
3
|
+
*
|
|
4
|
+
* General-purpose TTL cache with memory + SQLite backends.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CacheEntry, CacheBackend, TTLCacheOptions } from "../types.js";
|
|
8
|
+
|
|
9
|
+
// ─── Memory Backend ──────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
class MemoryBackend<K, V> implements CacheBackend<K, V> {
|
|
12
|
+
private store = new Map<K, CacheEntry<V>>();
|
|
13
|
+
private maxEntries: number;
|
|
14
|
+
|
|
15
|
+
constructor(maxEntries: number = 1000) {
|
|
16
|
+
this.maxEntries = maxEntries;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async get(key: K): Promise<V | undefined> {
|
|
20
|
+
const entry = this.store.get(key);
|
|
21
|
+
if (!entry) return undefined;
|
|
22
|
+
if (Date.now() > entry.expiresAt) {
|
|
23
|
+
this.store.delete(key);
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return entry.value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async set(key: K, value: V, ttlMs: number): Promise<void> {
|
|
30
|
+
// Evict oldest if at capacity
|
|
31
|
+
if (this.store.size >= this.maxEntries && !this.store.has(key)) {
|
|
32
|
+
const firstKey = this.store.keys().next().value;
|
|
33
|
+
if (firstKey !== undefined) {
|
|
34
|
+
this.store.delete(firstKey);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
this.store.set(key, {
|
|
40
|
+
value,
|
|
41
|
+
expiresAt: now + ttlMs,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async has(key: K): Promise<boolean> {
|
|
47
|
+
const entry = this.store.get(key);
|
|
48
|
+
if (!entry) return false;
|
|
49
|
+
if (Date.now() > entry.expiresAt) {
|
|
50
|
+
this.store.delete(key);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async delete(key: K): Promise<boolean> {
|
|
57
|
+
return this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async cleanupExpired(): Promise<number> {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
let count = 0;
|
|
63
|
+
for (const [key, entry] of this.store) {
|
|
64
|
+
if (now > entry.expiresAt) {
|
|
65
|
+
this.store.delete(key);
|
|
66
|
+
count++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return count;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async clear(): Promise<void> {
|
|
73
|
+
this.store.clear();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ─── SQLite Backend ──────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
// Minimal sqlite3 type declarations for lazy loading
|
|
80
|
+
interface Sqlite3Db {
|
|
81
|
+
run(sql: string, callback?: (err: Error | null) => void): Sqlite3Db;
|
|
82
|
+
run(sql: string, params: unknown[], callback?: (err: Error | null) => void): Sqlite3Db;
|
|
83
|
+
get(sql: string, params: unknown[], callback: (err: Error | null, row: unknown) => void): Sqlite3Db;
|
|
84
|
+
close(callback?: (err: Error | null) => void): void;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface Sqlite3 {
|
|
88
|
+
Database: new (path: string) => Sqlite3Db;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Lazy-load sqlite to avoid hard dependency
|
|
92
|
+
let sqlite3: Sqlite3 | null = null;
|
|
93
|
+
let sqliteLoadAttempted = false;
|
|
94
|
+
|
|
95
|
+
async function loadSqlite(): Promise<Sqlite3 | null> {
|
|
96
|
+
if (sqliteLoadAttempted) return sqlite3;
|
|
97
|
+
sqliteLoadAttempted = true;
|
|
98
|
+
try {
|
|
99
|
+
// Use dynamic import with type assertion to bypass module resolution
|
|
100
|
+
const mod = await eval("import('sqlite3')") as { default?: Sqlite3; Database?: unknown } | Sqlite3;
|
|
101
|
+
if (mod && typeof mod === "object") {
|
|
102
|
+
// Handle both ESM default export and CJS-style export
|
|
103
|
+
sqlite3 = (mod as { default?: Sqlite3 }).default ?? (mod as Sqlite3);
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
sqlite3 = null;
|
|
107
|
+
}
|
|
108
|
+
return sqlite3;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
class SQLiteBackend<K, V> implements CacheBackend<K, V> {
|
|
112
|
+
private db: Sqlite3Db | null = null;
|
|
113
|
+
private dbPath: string;
|
|
114
|
+
private ready: Promise<void>;
|
|
115
|
+
|
|
116
|
+
constructor(dbPath: string) {
|
|
117
|
+
this.dbPath = dbPath;
|
|
118
|
+
this.ready = this.init();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async init(): Promise<void> {
|
|
122
|
+
const sqlite = await loadSqlite();
|
|
123
|
+
if (!sqlite) {
|
|
124
|
+
throw new Error("sqlite3 not available for persistent cache");
|
|
125
|
+
}
|
|
126
|
+
this.db = new sqlite.Database(this.dbPath);
|
|
127
|
+
|
|
128
|
+
await new Promise<void>((resolve, reject) => {
|
|
129
|
+
this.db!.run(
|
|
130
|
+
`CREATE TABLE IF NOT EXISTS cache (
|
|
131
|
+
key TEXT PRIMARY KEY,
|
|
132
|
+
value TEXT NOT NULL,
|
|
133
|
+
expires_at INTEGER NOT NULL,
|
|
134
|
+
created_at INTEGER NOT NULL
|
|
135
|
+
)`,
|
|
136
|
+
(err: Error | null) => (err ? reject(err) : resolve()),
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Create index for fast expiration queries
|
|
141
|
+
await new Promise<void>((resolve, reject) => {
|
|
142
|
+
this.db!.run(
|
|
143
|
+
`CREATE INDEX IF NOT EXISTS idx_expires ON cache(expires_at)`,
|
|
144
|
+
(err: Error | null) => (err ? reject(err) : resolve()),
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async ensureReady(): Promise<void> {
|
|
150
|
+
await this.ready;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async get(key: K): Promise<V | undefined> {
|
|
154
|
+
await this.ensureReady();
|
|
155
|
+
if (!this.db) return undefined;
|
|
156
|
+
|
|
157
|
+
const row = await new Promise<{ value: string; expires_at: number } | undefined>(
|
|
158
|
+
(resolve, reject) => {
|
|
159
|
+
this.db!.get(
|
|
160
|
+
"SELECT value, expires_at FROM cache WHERE key = ?",
|
|
161
|
+
[String(key)],
|
|
162
|
+
(err: Error | null, row: unknown) => {
|
|
163
|
+
if (err) reject(err);
|
|
164
|
+
else resolve(row as { value: string; expires_at: number } | undefined);
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
if (!row) return undefined;
|
|
171
|
+
if (Date.now() > row.expires_at) {
|
|
172
|
+
await this.delete(key);
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(row.value) as V;
|
|
178
|
+
} catch {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async set(key: K, value: V, ttlMs: number): Promise<void> {
|
|
184
|
+
await this.ensureReady();
|
|
185
|
+
if (!this.db) return;
|
|
186
|
+
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
const expiresAt = now + ttlMs;
|
|
189
|
+
const serialized = JSON.stringify(value);
|
|
190
|
+
|
|
191
|
+
await new Promise<void>((resolve, reject) => {
|
|
192
|
+
this.db!.run(
|
|
193
|
+
`INSERT INTO cache (key, value, expires_at, created_at)
|
|
194
|
+
VALUES (?, ?, ?, ?)
|
|
195
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
196
|
+
value = excluded.value,
|
|
197
|
+
expires_at = excluded.expires_at,
|
|
198
|
+
created_at = excluded.created_at`,
|
|
199
|
+
[String(key), serialized, expiresAt, now],
|
|
200
|
+
(err: Error | null) => (err ? reject(err) : resolve()),
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async has(key: K): Promise<boolean> {
|
|
206
|
+
const value = await this.get(key);
|
|
207
|
+
return value !== undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async delete(key: K): Promise<boolean> {
|
|
211
|
+
await this.ensureReady();
|
|
212
|
+
if (!this.db) return false;
|
|
213
|
+
|
|
214
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
215
|
+
this.db!.run(
|
|
216
|
+
"DELETE FROM cache WHERE key = ?",
|
|
217
|
+
[String(key)],
|
|
218
|
+
function (this: { changes: number }, err: Error | null) {
|
|
219
|
+
if (err) reject(err);
|
|
220
|
+
else resolve(this.changes > 0);
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async cleanupExpired(): Promise<number> {
|
|
227
|
+
await this.ensureReady();
|
|
228
|
+
if (!this.db) return 0;
|
|
229
|
+
|
|
230
|
+
return new Promise<number>((resolve, reject) => {
|
|
231
|
+
this.db!.run(
|
|
232
|
+
"DELETE FROM cache WHERE expires_at <= ?",
|
|
233
|
+
[Date.now()],
|
|
234
|
+
function (this: { changes: number }, err: Error | null) {
|
|
235
|
+
if (err) reject(err);
|
|
236
|
+
else resolve(this.changes);
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async clear(): Promise<void> {
|
|
243
|
+
await this.ensureReady();
|
|
244
|
+
if (!this.db) return;
|
|
245
|
+
|
|
246
|
+
await new Promise<void>((resolve, reject) => {
|
|
247
|
+
this.db!.run("DELETE FROM cache", (err: Error | null) => (err ? reject(err) : resolve()));
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── TTL Cache ───────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/** Default options */
|
|
255
|
+
const DEFAULTS: Required<TTLCacheOptions> = {
|
|
256
|
+
persistent: false,
|
|
257
|
+
dbPath: "",
|
|
258
|
+
defaultTtlMs: 3600000, // 1 hour
|
|
259
|
+
maxMemoryEntries: 1000,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* General-purpose TTL cache with optional SQLite persistence.
|
|
264
|
+
*/
|
|
265
|
+
export class TTLCache<K = string, V = unknown> {
|
|
266
|
+
private backend: CacheBackend<K, V>;
|
|
267
|
+
private opts: Required<TTLCacheOptions>;
|
|
268
|
+
|
|
269
|
+
constructor(options: TTLCacheOptions = {}) {
|
|
270
|
+
this.opts = { ...DEFAULTS, ...options };
|
|
271
|
+
|
|
272
|
+
if (this.opts.persistent) {
|
|
273
|
+
const dbPath =
|
|
274
|
+
this.opts.dbPath ||
|
|
275
|
+
new URL("~/.unipi/cache/ttl-cache.db", import.meta.url).pathname;
|
|
276
|
+
this.backend = new SQLiteBackend<K, V>(dbPath);
|
|
277
|
+
} else {
|
|
278
|
+
this.backend = new MemoryBackend<K, V>(this.opts.maxMemoryEntries);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Get a value by key */
|
|
283
|
+
async get(key: K): Promise<V | undefined> {
|
|
284
|
+
return this.backend.get(key);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Set a value with TTL */
|
|
288
|
+
async set(key: K, value: V, ttlMs?: number): Promise<void> {
|
|
289
|
+
return this.backend.set(key, value, ttlMs ?? this.opts.defaultTtlMs);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Check if key exists and is not expired */
|
|
293
|
+
async has(key: K): Promise<boolean> {
|
|
294
|
+
return this.backend.has(key);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Delete a key */
|
|
298
|
+
async delete(key: K): Promise<boolean> {
|
|
299
|
+
return this.backend.delete(key);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Clean up all expired entries */
|
|
303
|
+
async cleanupExpired(): Promise<number> {
|
|
304
|
+
return this.backend.cleanupExpired();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Clear all entries */
|
|
308
|
+
async clear(): Promise<void> {
|
|
309
|
+
return this.backend.clear();
|
|
310
|
+
}
|
|
311
|
+
}
|
package/src/commands.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Command registration
|
|
3
|
+
*
|
|
4
|
+
* Registers all utility commands:
|
|
5
|
+
* - /unipi:continue — existing, preserved
|
|
6
|
+
* - /unipi:reload — reload all extensions
|
|
7
|
+
* - /unipi:status — show module status
|
|
8
|
+
* - /unipi:cleanup — clean stale files
|
|
9
|
+
* - /unipi:env — show environment
|
|
10
|
+
* - /unipi:doctor — run diagnostics
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import {
|
|
15
|
+
UNIPI_PREFIX,
|
|
16
|
+
UTILITY_COMMANDS,
|
|
17
|
+
UNIPI_EVENTS,
|
|
18
|
+
emitEvent,
|
|
19
|
+
} from "@pi-unipi/core";
|
|
20
|
+
import { cleanupStale, formatCleanupReport } from "./lifecycle/cleanup.js";
|
|
21
|
+
import { runDiagnostics, formatDiagnosticsReport } from "./diagnostics/engine.js";
|
|
22
|
+
import { getEnvironmentInfo, formatEnvironmentInfo } from "./tools/env.js";
|
|
23
|
+
|
|
24
|
+
/** Send a markdown response via pi.sendMessage */
|
|
25
|
+
function sendResponse(pi: ExtensionAPI, markdown: string): void {
|
|
26
|
+
pi.sendMessage(
|
|
27
|
+
{
|
|
28
|
+
customType: "unipi-response",
|
|
29
|
+
content: markdown,
|
|
30
|
+
display: true,
|
|
31
|
+
},
|
|
32
|
+
{ deliverAs: "followUp" },
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Register all utility commands.
|
|
38
|
+
*/
|
|
39
|
+
export function registerUtilityCommands(pi: ExtensionAPI): void {
|
|
40
|
+
// ─── /unipi:continue — preserved ─────────────────────────────────────────
|
|
41
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.CONTINUE}`, {
|
|
42
|
+
description: "Continue the agent from where it left off without adding user context",
|
|
43
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
44
|
+
if (!ctx.isIdle()) {
|
|
45
|
+
if (ctx.hasUI) {
|
|
46
|
+
ctx.ui.notify(
|
|
47
|
+
"Agent is busy. Press ESC to interrupt, then try again.",
|
|
48
|
+
"warning",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
pi.sendMessage(
|
|
55
|
+
{
|
|
56
|
+
customType: "unipi-continue",
|
|
57
|
+
content: "",
|
|
58
|
+
display: false,
|
|
59
|
+
},
|
|
60
|
+
{ triggerTurn: true },
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ─── /unipi:reload ───────────────────────────────────────────────────────
|
|
66
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.RELOAD}`, {
|
|
67
|
+
description: "Reload all Pi extensions without restarting",
|
|
68
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
69
|
+
if (!ctx.isIdle()) {
|
|
70
|
+
if (ctx.hasUI) {
|
|
71
|
+
ctx.ui.notify("Agent is busy. Press ESC to interrupt first.", "warning");
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
sendResponse(
|
|
77
|
+
pi,
|
|
78
|
+
"## 🔄 Reload Extensions\n\n" +
|
|
79
|
+
"To reload all extensions:\n" +
|
|
80
|
+
"1. Press **Ctrl+C** to exit Pi\n" +
|
|
81
|
+
"2. Run `pi` again to restart with fresh extensions\n\n" +
|
|
82
|
+
"*Note: Pi does not support hot-reloading of extensions.*",
|
|
83
|
+
);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── /unipi:status ───────────────────────────────────────────────────────
|
|
88
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.STATUS}`, {
|
|
89
|
+
description: "Show all unipi modules status",
|
|
90
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
91
|
+
if (!ctx.isIdle()) {
|
|
92
|
+
if (ctx.hasUI) {
|
|
93
|
+
ctx.ui.notify("Agent is busy. Press ESC to interrupt first.", "warning");
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Request status from all modules — responses will be logged
|
|
99
|
+
const requestId = `status-${Date.now()}`;
|
|
100
|
+
emitEvent(pi, UNIPI_EVENTS.MODULE_STATUS_REQUEST, { requestId });
|
|
101
|
+
|
|
102
|
+
// Give modules a moment to respond, then show what we know
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
104
|
+
|
|
105
|
+
sendResponse(
|
|
106
|
+
pi,
|
|
107
|
+
"## 📊 Module Status\n\n" +
|
|
108
|
+
"Status request broadcast to all modules.\n" +
|
|
109
|
+
"Modules that support status reporting will respond via events.\n\n" +
|
|
110
|
+
`*Request ID: \`${requestId}\`*`,
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ─── /unipi:cleanup ──────────────────────────────────────────────────────
|
|
116
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.CLEANUP}`, {
|
|
117
|
+
description: "Clean temp files, stale DBs, old sessions",
|
|
118
|
+
handler: async (args: string, ctx: ExtensionContext) => {
|
|
119
|
+
if (!ctx.isIdle()) {
|
|
120
|
+
if (ctx.hasUI) {
|
|
121
|
+
ctx.ui.notify("Agent is busy. Press ESC to interrupt first.", "warning");
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const dryRun = args.includes("--dry-run");
|
|
127
|
+
const report = cleanupStale({ dryRun });
|
|
128
|
+
|
|
129
|
+
emitEvent(pi, UNIPI_EVENTS.UTILITY_CLEANUP_DONE, {
|
|
130
|
+
dryRun,
|
|
131
|
+
categories: ["db", "temp", "session", "cache"],
|
|
132
|
+
results: report.results.map((r) => ({
|
|
133
|
+
category: r.category,
|
|
134
|
+
removed: r.removed,
|
|
135
|
+
bytesFreed: r.bytesFreed,
|
|
136
|
+
})),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
sendResponse(pi, formatCleanupReport(report));
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ─── /unipi:env ──────────────────────────────────────────────────────────
|
|
144
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.ENV}`, {
|
|
145
|
+
description: "Show environment info (versions, paths)",
|
|
146
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
147
|
+
if (!ctx.isIdle()) {
|
|
148
|
+
if (ctx.hasUI) {
|
|
149
|
+
ctx.ui.notify("Agent is busy. Press ESC to interrupt first.", "warning");
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const info = getEnvironmentInfo();
|
|
155
|
+
sendResponse(pi, formatEnvironmentInfo(info));
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ─── /unipi:doctor ───────────────────────────────────────────────────────
|
|
160
|
+
pi.registerCommand(`${UNIPI_PREFIX}${UTILITY_COMMANDS.DOCTOR}`, {
|
|
161
|
+
description: "Run diagnostics across all unipi modules",
|
|
162
|
+
handler: async (_args: string, ctx: ExtensionContext) => {
|
|
163
|
+
if (!ctx.isIdle()) {
|
|
164
|
+
if (ctx.hasUI) {
|
|
165
|
+
ctx.ui.notify("Agent is busy. Press ESC to interrupt first.", "warning");
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
emitEvent(pi, UNIPI_EVENTS.UTILITY_DIAGNOSTICS_START, {
|
|
171
|
+
overall: "unknown",
|
|
172
|
+
checkCount: 0,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const report = await runDiagnostics();
|
|
176
|
+
|
|
177
|
+
emitEvent(pi, UNIPI_EVENTS.UTILITY_DIAGNOSTICS_DONE, {
|
|
178
|
+
overall: report.overall,
|
|
179
|
+
checkCount: report.checks.length,
|
|
180
|
+
report,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
sendResponse(pi, formatDiagnosticsReport(report));
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|