decorated-pi 0.4.1 → 0.5.0
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 +74 -57
- package/extensions/guidance.ts +20 -13
- package/extensions/index.ts +72 -6
- package/extensions/io.ts +4 -3
- package/extensions/lsp/servers.ts +63 -3
- package/extensions/mcp/builtin.ts +348 -42
- package/extensions/mcp/client.ts +28 -19
- package/extensions/mcp/index.ts +407 -80
- package/extensions/model-integration.ts +19 -8
- package/extensions/rtk.ts +244 -0
- package/extensions/settings.ts +63 -1
- package/extensions/slash.ts +199 -67
- package/extensions/smart-at.ts +27 -1
- package/extensions/wakatime.ts +403 -0
- package/package.json +4 -5
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WakaTime — coding activity heartbeats for Pi sessions
|
|
3
|
+
*
|
|
4
|
+
* Reads API key from ~/.wakatime.cfg and sends heartbeats to WakaTime using a
|
|
5
|
+
* small runtime state machine:
|
|
6
|
+
* - before_agent_start → mark app activity
|
|
7
|
+
* - tool_result → switch active entity/category based on real work
|
|
8
|
+
* - agent_end → stop immediate activity, keepalive handles continuity
|
|
9
|
+
* - keepalive timer → extend continuous activity while not idle
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
14
|
+
import * as fs from "node:fs";
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
const MACHINE_NAME = os.hostname();
|
|
20
|
+
const PACKAGE_VERSION = readPackageVersion();
|
|
21
|
+
|
|
22
|
+
// ─── Config ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
const WAKATIME_CFG = path.join(os.homedir(), ".wakatime.cfg");
|
|
25
|
+
const WAKATIME_CLI_FALLBACK = path.join(os.homedir(), ".wakatime", "wakatime-cli");
|
|
26
|
+
const API_URL = "https://api.wakatime.com/api/v1";
|
|
27
|
+
const KEEPALIVE_MS = 90 * 1000;
|
|
28
|
+
const IDLE_TIMEOUT_MS = 2 * 60 * 1000;
|
|
29
|
+
const TIMER_TICK_MS = 15 * 1000;
|
|
30
|
+
|
|
31
|
+
function readPackageVersion(): string {
|
|
32
|
+
try {
|
|
33
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
34
|
+
const pkgPath = path.join(here, "..", "package.json");
|
|
35
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version?: string };
|
|
36
|
+
return pkg.version?.trim() || "unknown";
|
|
37
|
+
} catch {
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readWakatimeCfgApiKey(configPath = WAKATIME_CFG): string | undefined {
|
|
43
|
+
try {
|
|
44
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
45
|
+
const match = content.match(/^api_key\s*=\s*(.+)$/m);
|
|
46
|
+
return match?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getApiKey(): string | undefined {
|
|
53
|
+
return readWakatimeCfgApiKey();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Heartbeat types ───────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
interface Heartbeat {
|
|
59
|
+
entity: string;
|
|
60
|
+
time: number;
|
|
61
|
+
type: "file" | "app";
|
|
62
|
+
category: "ai coding" | "building" | "running tests";
|
|
63
|
+
project?: string;
|
|
64
|
+
project_root_count?: number;
|
|
65
|
+
language?: string;
|
|
66
|
+
lines?: number;
|
|
67
|
+
is_write?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ActiveState {
|
|
71
|
+
heartbeat: Omit<Heartbeat, "time">;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
lastActivityAt: number;
|
|
74
|
+
lastHeartbeatAt: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type HeartbeatSender = (hb: Heartbeat, cwd?: string) => void;
|
|
78
|
+
|
|
79
|
+
let active: ActiveState | null = null;
|
|
80
|
+
let timer: ReturnType<typeof setInterval> | null = null;
|
|
81
|
+
let terminalInputUnsub: (() => void) | null = null;
|
|
82
|
+
let cachedWakatimeCliPath: string | null | undefined;
|
|
83
|
+
|
|
84
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function extToLanguage(filePath: string): string | undefined {
|
|
87
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
88
|
+
const map: Record<string, string> = {
|
|
89
|
+
".ts": "TypeScript", ".tsx": "TypeScript", ".js": "JavaScript",
|
|
90
|
+
".jsx": "JavaScript", ".py": "Python", ".rs": "Rust",
|
|
91
|
+
".go": "Go", ".rb": "Ruby", ".java": "Java", ".c": "C",
|
|
92
|
+
".cpp": "C++", ".cc": "C++", ".cxx": "C++", ".h": "C", ".hpp": "C++",
|
|
93
|
+
".json": "JSON", ".yaml": "YAML", ".yml": "YAML", ".toml": "TOML",
|
|
94
|
+
".md": "Markdown", ".css": "CSS", ".html": "HTML", ".svelte": "Svelte",
|
|
95
|
+
".lua": "Lua", ".sh": "Bash", ".sql": "SQL", ".xml": "XML",
|
|
96
|
+
};
|
|
97
|
+
return map[ext];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function countPathParts(p: string): number {
|
|
101
|
+
return p.split(/[\\/]+/).filter(Boolean).length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function buildProjectMeta(absPath: string, cwd: string): Pick<Heartbeat, "project" | "project_root_count"> {
|
|
105
|
+
const root = path.resolve(cwd);
|
|
106
|
+
const inProject = absPath.startsWith(root + path.sep) || absPath === root;
|
|
107
|
+
return {
|
|
108
|
+
project: inProject ? (path.basename(root) || undefined) : undefined,
|
|
109
|
+
project_root_count: inProject ? countPathParts(root) : undefined,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function countLines(absPath: string): number | undefined {
|
|
114
|
+
try {
|
|
115
|
+
const text = fs.readFileSync(absPath, "utf-8");
|
|
116
|
+
return text.split(/\r?\n/).length;
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function classifyBash(command: string | undefined): Heartbeat["category"] {
|
|
123
|
+
const cmd = String(command ?? "").trim();
|
|
124
|
+
if (!cmd) return "ai coding";
|
|
125
|
+
if (/\b(make|cmake|ninja|npm run build|pnpm build|yarn build|cargo build|go build)\b/i.test(cmd)) {
|
|
126
|
+
return "building";
|
|
127
|
+
}
|
|
128
|
+
if (/\b(pytest|vitest|jest|npm test|pnpm test|yarn test|go test|ctest|cargo test)\b/i.test(cmd)) {
|
|
129
|
+
return "running tests";
|
|
130
|
+
}
|
|
131
|
+
return "ai coding";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildPluginString(version = PACKAGE_VERSION): string {
|
|
135
|
+
return `pi/${version} pi/${version}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findWakatimeCliOnPath(): string | null {
|
|
139
|
+
try {
|
|
140
|
+
if (process.platform === "win32") {
|
|
141
|
+
const output = execFileSync("where", ["wakatime-cli"], { encoding: "utf-8" }).trim();
|
|
142
|
+
const first = output.split(/\r?\n/)[0]?.trim();
|
|
143
|
+
return first ? path.resolve(first) : null;
|
|
144
|
+
}
|
|
145
|
+
const shell = process.env.SHELL || "sh";
|
|
146
|
+
const output = execFileSync(shell, ["-lc", "command -v wakatime-cli"], { encoding: "utf-8" }).trim();
|
|
147
|
+
return output ? path.resolve(output) : null;
|
|
148
|
+
} catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function findWakatimeCli(options: {
|
|
154
|
+
probePath?: () => string | null;
|
|
155
|
+
exists?: (candidate: string) => boolean;
|
|
156
|
+
fallbackPath?: string;
|
|
157
|
+
} = {}): string | null {
|
|
158
|
+
if (cachedWakatimeCliPath !== undefined) return cachedWakatimeCliPath;
|
|
159
|
+
|
|
160
|
+
const probePath = options.probePath ?? findWakatimeCliOnPath;
|
|
161
|
+
const exists = options.exists ?? fs.existsSync;
|
|
162
|
+
const fromPath = probePath();
|
|
163
|
+
if (fromPath) {
|
|
164
|
+
cachedWakatimeCliPath = path.resolve(fromPath);
|
|
165
|
+
return cachedWakatimeCliPath;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const fallback = path.resolve(options.fallbackPath ?? WAKATIME_CLI_FALLBACK);
|
|
169
|
+
cachedWakatimeCliPath = exists(fallback) ? fallback : null;
|
|
170
|
+
return cachedWakatimeCliPath;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function buildCliArgs(
|
|
174
|
+
hb: Heartbeat,
|
|
175
|
+
apiKey: string,
|
|
176
|
+
cwd?: string,
|
|
177
|
+
plugin = buildPluginString(),
|
|
178
|
+
): string[] {
|
|
179
|
+
const args = [
|
|
180
|
+
"--entity", hb.entity,
|
|
181
|
+
"--entity-type", hb.type,
|
|
182
|
+
"--category", hb.category,
|
|
183
|
+
"--plugin", plugin,
|
|
184
|
+
"--key", apiKey,
|
|
185
|
+
"--time", String(hb.time),
|
|
186
|
+
"--hostname", MACHINE_NAME,
|
|
187
|
+
];
|
|
188
|
+
|
|
189
|
+
const shouldSendProjectFolder = hb.type === "app" || !!hb.project || typeof hb.project_root_count === "number";
|
|
190
|
+
if (cwd && shouldSendProjectFolder) args.push("--project-folder", cwd);
|
|
191
|
+
if (hb.project) args.push("--project", hb.project);
|
|
192
|
+
if (hb.language) args.push("--language", hb.language);
|
|
193
|
+
if (typeof hb.lines === "number") args.push("--lines-in-file", String(hb.lines));
|
|
194
|
+
if (hb.is_write) args.push("--write");
|
|
195
|
+
return args;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sendHeartbeatViaCli(hb: Heartbeat, apiKey: string, cliPath: string, cwd?: string): void {
|
|
199
|
+
const args = buildCliArgs(hb, apiKey, cwd);
|
|
200
|
+
execFile(cliPath, args, {
|
|
201
|
+
timeout: 10_000,
|
|
202
|
+
windowsHide: true,
|
|
203
|
+
}, () => {
|
|
204
|
+
// Silent: activity tracking must never interrupt the agent.
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function heartbeatChanged(a: Omit<Heartbeat, "time">, b: Omit<Heartbeat, "time">): boolean {
|
|
209
|
+
return (
|
|
210
|
+
a.entity !== b.entity ||
|
|
211
|
+
a.type !== b.type ||
|
|
212
|
+
a.category !== b.category ||
|
|
213
|
+
a.project !== b.project ||
|
|
214
|
+
a.project_root_count !== b.project_root_count ||
|
|
215
|
+
a.language !== b.language
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function switchActive(
|
|
220
|
+
next: Omit<Heartbeat, "time">,
|
|
221
|
+
sendHeartbeat: HeartbeatSender,
|
|
222
|
+
options: { immediate?: boolean; isWrite?: boolean; cwd?: string } = {},
|
|
223
|
+
): void {
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
const changed = !active || heartbeatChanged(active.heartbeat, next);
|
|
226
|
+
|
|
227
|
+
active = {
|
|
228
|
+
heartbeat: next,
|
|
229
|
+
cwd: options.cwd,
|
|
230
|
+
lastActivityAt: now,
|
|
231
|
+
lastHeartbeatAt: active?.lastHeartbeatAt ?? 0,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (changed || options.immediate || options.isWrite) {
|
|
235
|
+
sendHeartbeat({ ...next, time: now / 1000, is_write: options.isWrite }, options.cwd);
|
|
236
|
+
active.lastHeartbeatAt = now;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function sendOneShot(hb: Omit<Heartbeat, "time">, sendHeartbeat: HeartbeatSender, isWrite?: boolean, cwd?: string): void {
|
|
241
|
+
sendHeartbeat({ ...hb, time: Date.now() / 1000, is_write: isWrite }, cwd);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function touchActivity(): void {
|
|
245
|
+
if (active) active.lastActivityAt = Date.now();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function ensureTimer(sendHeartbeat: HeartbeatSender): void {
|
|
249
|
+
if (timer) return;
|
|
250
|
+
timer = setInterval(() => {
|
|
251
|
+
if (!active) return;
|
|
252
|
+
const now = Date.now();
|
|
253
|
+
if (now - active.lastActivityAt > IDLE_TIMEOUT_MS) return;
|
|
254
|
+
if (now - active.lastHeartbeatAt < KEEPALIVE_MS) return;
|
|
255
|
+
sendHeartbeat({ ...active.heartbeat, time: now / 1000 }, active.cwd);
|
|
256
|
+
active.lastHeartbeatAt = now;
|
|
257
|
+
}, TIMER_TICK_MS);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function clearTimer(): void {
|
|
261
|
+
if (timer) clearInterval(timer);
|
|
262
|
+
timer = null;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function buildAppHeartbeat(cwd: string, category: Heartbeat["category"] = "ai coding"): Omit<Heartbeat, "time"> {
|
|
266
|
+
const root = path.resolve(cwd);
|
|
267
|
+
return {
|
|
268
|
+
entity: "pi",
|
|
269
|
+
type: "app",
|
|
270
|
+
category,
|
|
271
|
+
project: path.basename(root) || undefined,
|
|
272
|
+
project_root_count: countPathParts(root),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function buildFileHeartbeat(absPath: string, cwd: string, category: Heartbeat["category"] = "ai coding"): Omit<Heartbeat, "time"> {
|
|
277
|
+
const meta = buildProjectMeta(absPath, cwd);
|
|
278
|
+
return {
|
|
279
|
+
entity: absPath,
|
|
280
|
+
type: "file",
|
|
281
|
+
category,
|
|
282
|
+
project: meta.project,
|
|
283
|
+
project_root_count: meta.project_root_count,
|
|
284
|
+
language: extToLanguage(absPath),
|
|
285
|
+
lines: countLines(absPath),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ─── Setup ──────────────────────────────────────────────────────────────────
|
|
290
|
+
|
|
291
|
+
export function resetWakatimeStateForTests(): void {
|
|
292
|
+
active = null;
|
|
293
|
+
clearTimer();
|
|
294
|
+
if (terminalInputUnsub) terminalInputUnsub();
|
|
295
|
+
terminalInputUnsub = null;
|
|
296
|
+
cachedWakatimeCliPath = undefined;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function setupWakatimeWithApiKey(
|
|
300
|
+
pi: ExtensionAPI,
|
|
301
|
+
apiKey: string | undefined,
|
|
302
|
+
cliPath: string | null | undefined = findWakatimeCli(),
|
|
303
|
+
sendHeartbeat: HeartbeatSender | undefined = cliPath && apiKey
|
|
304
|
+
? (hb, cwd) => sendHeartbeatViaCli(hb, apiKey, cliPath, cwd)
|
|
305
|
+
: undefined,
|
|
306
|
+
): void {
|
|
307
|
+
if (!apiKey || !cliPath || !sendHeartbeat) return;
|
|
308
|
+
|
|
309
|
+
pi.on("session_start", (_event, ctx) => {
|
|
310
|
+
active = null;
|
|
311
|
+
ensureTimer(sendHeartbeat);
|
|
312
|
+
if (terminalInputUnsub) terminalInputUnsub();
|
|
313
|
+
terminalInputUnsub = null;
|
|
314
|
+
if (!ctx.hasUI) return;
|
|
315
|
+
|
|
316
|
+
terminalInputUnsub = ctx.ui.onTerminalInput(() => {
|
|
317
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
318
|
+
const recent = active && (Date.now() - active.lastActivityAt) <= IDLE_TIMEOUT_MS;
|
|
319
|
+
const isAppHeartbeat = active?.heartbeat.type === "app" && active.heartbeat.entity === "pi";
|
|
320
|
+
if (!recent || !isAppHeartbeat) {
|
|
321
|
+
switchActive(buildAppHeartbeat(cwd), sendHeartbeat, { immediate: true, cwd });
|
|
322
|
+
} else {
|
|
323
|
+
touchActivity();
|
|
324
|
+
}
|
|
325
|
+
return undefined;
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
pi.on("session_shutdown", () => {
|
|
330
|
+
active = null;
|
|
331
|
+
clearTimer();
|
|
332
|
+
if (terminalInputUnsub) terminalInputUnsub();
|
|
333
|
+
terminalInputUnsub = null;
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
pi.on("before_agent_start", (_event, ctx) => {
|
|
337
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
338
|
+
|
|
339
|
+
// Keep the long-lived active heartbeat on the app entity (`pi`) so
|
|
340
|
+
// keepalive time accrues to the Pi editor itself. File heartbeats are sent
|
|
341
|
+
// separately as one-shots below; if we replaced `active` with a file entity,
|
|
342
|
+
// the dashboard would attribute nearly all time to the last touched file and
|
|
343
|
+
// Pi editor time would collapse toward zero.
|
|
344
|
+
switchActive(buildAppHeartbeat(cwd), sendHeartbeat, { immediate: true, cwd });
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
pi.on("tool_result", (event, ctx) => {
|
|
348
|
+
const cwd = ctx.cwd ?? process.cwd();
|
|
349
|
+
const input = (event as any).input;
|
|
350
|
+
|
|
351
|
+
// File-level operations send one-shot heartbeats but never replace the
|
|
352
|
+
// keepalive entity — the app heartbeat stays active for accurate editor
|
|
353
|
+
// time attribution on the WakaTime dashboard.
|
|
354
|
+
if (event.toolName === "read") {
|
|
355
|
+
const filePath = input?.path ?? input?.file ?? input?.file_path;
|
|
356
|
+
if (typeof filePath !== "string" || !filePath.trim()) return;
|
|
357
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
358
|
+
sendOneShot(buildFileHeartbeat(absPath, cwd), sendHeartbeat, undefined, cwd);
|
|
359
|
+
touchActivity();
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (event.toolName === "patch") {
|
|
364
|
+
const filePath = input?.path;
|
|
365
|
+
if (typeof filePath !== "string" || !filePath.trim()) return;
|
|
366
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
367
|
+
sendOneShot(buildFileHeartbeat(absPath, cwd), sendHeartbeat, true, cwd);
|
|
368
|
+
touchActivity();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (event.toolName === "lsp_document_symbols") {
|
|
373
|
+
const filePath = input?.path;
|
|
374
|
+
if (typeof filePath !== "string" || !filePath.trim()) return;
|
|
375
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
|
|
376
|
+
sendOneShot(buildFileHeartbeat(absPath, cwd), sendHeartbeat, undefined, cwd);
|
|
377
|
+
touchActivity();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (event.toolName === "lsp_diagnostics") {
|
|
382
|
+
touchActivity();
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (event.toolName === "bash") {
|
|
387
|
+
const category = classifyBash(input?.command);
|
|
388
|
+
switchActive(buildAppHeartbeat(cwd, category), sendHeartbeat, { cwd });
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
touchActivity();
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
pi.on("agent_end", (_event, _ctx) => {
|
|
396
|
+
// Do not clear immediately; keepalive continues briefly until idle timeout.
|
|
397
|
+
touchActivity();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function setupWakatime(pi: ExtensionAPI) {
|
|
402
|
+
setupWakatimeWithApiKey(pi, getApiKey(), findWakatimeCli());
|
|
403
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "decorated-pi",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "decorated-pi is a practical enhancement pack for pi — smarter tools that are token efficient and cache friendly.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
7
7
|
"pi-package",
|
|
8
8
|
"pi-extension",
|
|
9
|
-
"
|
|
9
|
+
"rtk",
|
|
10
|
+
"mcp",
|
|
10
11
|
"security",
|
|
11
12
|
"lsp",
|
|
12
13
|
"language-server",
|
|
13
|
-
"autocomplete",
|
|
14
|
-
"entropy",
|
|
15
14
|
"secret-detection"
|
|
16
15
|
],
|
|
17
16
|
"license": "MIT",
|