insights and other extension fixes
This commit is contained in:
parent
8bd65f7313
commit
ca9acbdffb
8 changed files with 955 additions and 4 deletions
174
.pi/agent/extensions/insights/index.ts
Normal file
174
.pi/agent/extensions/insights/index.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import * as fs from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const EXT_DIR = path.dirname(new URL(import.meta.url).pathname);
|
||||
const ANALYZE = path.join(EXT_DIR, "analyze.py");
|
||||
const RENDER = path.join(EXT_DIR, "render.py");
|
||||
const OUT_DIR = path.join(os.homedir(), ".pi", "agent", "usage-data");
|
||||
const STATS_PATH = path.join(OUT_DIR, "stats.json");
|
||||
const NARR_PATH = path.join(OUT_DIR, "narrative.json");
|
||||
// The report is written to the shared /workspace mount so it's accessible
|
||||
// from the host machine.
|
||||
const REPORT_DIR = "/workspace";
|
||||
const REPORT_PATH = path.join(REPORT_DIR, "pi-insights-report.html");
|
||||
|
||||
// /workspace inside the sandbox maps to this path on the host machine.
|
||||
const SANDBOX_MOUNT = "/workspace";
|
||||
const HOST_MOUNT = "/Users/aselimov/rescale";
|
||||
|
||||
// Return a clickable file:// URL for the report instead of launching a
|
||||
// browser. Spawning an opener (xdg-open/open) is unreliable in headless
|
||||
// sandboxes and a missing binary surfaces as an uncaught async error event.
|
||||
// The URL is rewritten to the host path so it's clickable from the user's
|
||||
// real computer rather than referencing the container filesystem.
|
||||
function reportUrl(file: string) {
|
||||
const hostPath = file.startsWith(SANDBOX_MOUNT)
|
||||
? HOST_MOUNT + file.slice(SANDBOX_MOUNT.length)
|
||||
: file;
|
||||
return pathToFileURL(hostPath).href;
|
||||
}
|
||||
|
||||
function renderReport() {
|
||||
execFileSync("python3", [RENDER, STATS_PATH, NARR_PATH, REPORT_PATH], {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
}
|
||||
|
||||
export default function (pi: ExtensionAPI) {
|
||||
// Tool the LLM calls to supply the qualitative narrative.
|
||||
pi.registerTool({
|
||||
name: "write_insights_report",
|
||||
label: "Write Insights Report",
|
||||
description:
|
||||
"Write the qualitative narrative for the pi usage insights report, then " +
|
||||
"render and open the final HTML. Call this exactly once after analyzing the stats.",
|
||||
parameters: Type.Object({
|
||||
summary: Type.String({
|
||||
description: "One or two sentence overview of the user's pi usage.",
|
||||
}),
|
||||
highlights: Type.Array(Type.String(), {
|
||||
description: "Notable facts about how they use pi (3-6 items).",
|
||||
}),
|
||||
friction: Type.Array(Type.String(), {
|
||||
description: "Observed friction patterns or inefficiencies (0-5 items).",
|
||||
}),
|
||||
suggestions: Type.Array(
|
||||
Type.Object({
|
||||
title: Type.String(),
|
||||
detail: Type.String(),
|
||||
}),
|
||||
{ description: "Concrete, actionable workflow suggestions (2-5 items)." }
|
||||
),
|
||||
}),
|
||||
async execute(_id, params) {
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.writeFileSync(NARR_PATH, JSON.stringify(params, null, 2));
|
||||
renderReport();
|
||||
const url = reportUrl(REPORT_PATH);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: `Insights report written. Open it here:\n${url}`,
|
||||
},
|
||||
],
|
||||
details: { reportPath: REPORT_PATH, url },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("insights", {
|
||||
description: "Analyze your recent pi sessions and generate an insights report (optional: number of days, default 30)",
|
||||
handler: async (args, ctx) => {
|
||||
const days = (args || "").trim().match(/^\d+$/) ? args.trim() : "30";
|
||||
ctx.ui.setStatus("insights", "Analyzing sessions…");
|
||||
|
||||
let statsRaw: string;
|
||||
try {
|
||||
statsRaw = execFileSync("python3", [ANALYZE, days], { encoding: "utf-8" });
|
||||
} catch (e: any) {
|
||||
ctx.ui.setStatus("insights", "");
|
||||
ctx.ui.notify(`insights: analysis failed: ${e?.message ?? e}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
fs.writeFileSync(STATS_PATH, statsRaw);
|
||||
|
||||
let stats: any;
|
||||
try {
|
||||
stats = JSON.parse(statsRaw);
|
||||
} catch {
|
||||
ctx.ui.setStatus("insights", "");
|
||||
ctx.ui.notify("insights: could not parse analyzer output", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.ui.setStatus("insights", "");
|
||||
|
||||
if (!stats?.totals?.sessions) {
|
||||
// Render a stats-only report (no LLM narrative needed).
|
||||
if (fs.existsSync(NARR_PATH)) fs.rmSync(NARR_PATH);
|
||||
renderReport();
|
||||
ctx.ui.notify(
|
||||
`No sessions found in the last ${days} days. Empty report: ${reportUrl(REPORT_PATH)}`,
|
||||
"info"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const t = stats.totals;
|
||||
const topProjects = (stats.projects || [])
|
||||
.slice(0, 8)
|
||||
.map((p: any) => `${p.project} (${p.sessions} sessions, ${p.tool_calls} tool calls)`)
|
||||
.join(", ");
|
||||
const topTools = (stats.tool_usage || [])
|
||||
.slice(0, 8)
|
||||
.map((x: any) => `${x.name}=${x.count}`)
|
||||
.join(", ");
|
||||
const peakHours = (stats.hour_histogram || [])
|
||||
.map((c: number, h: number) => ({ h, c }))
|
||||
.sort((a: any, b: any) => b.c - a.c)
|
||||
.slice(0, 3)
|
||||
.map((x: any) => `${x.h}:00`)
|
||||
.join(", ");
|
||||
|
||||
const prompt = [
|
||||
"You are generating a pi usage insights report (like Claude Code's /insights).",
|
||||
"Below are deterministic statistics computed from the user's pi session history",
|
||||
`over the last ${days} days. Analyze them and then call the`,
|
||||
"`write_insights_report` tool exactly once with a concise narrative.",
|
||||
"Be specific, reference the actual numbers, and make suggestions actionable",
|
||||
"(e.g. /compact usage, AGENTS.md rules, prompt templates, model choice,",
|
||||
"running sessions inside the right project dir). Do not write any prose to the",
|
||||
"chat beyond a one-line confirmation; put everything in the tool call.",
|
||||
"",
|
||||
"KEY STATS:",
|
||||
`- Sessions: ${t.sessions} across ${stats.active_days} active days`,
|
||||
`- Prompts: ${t.user_msgs}, assistant turns: ${t.assistant_msgs}, tool calls: ${t.tool_calls}`,
|
||||
`- Tokens: ${t.total_tokens} (cacheRead ${t.cache_read}), cost: $${t.cost}`,
|
||||
`- Avg session duration: ${stats.avg_session_duration_min ?? "n/a"} min`,
|
||||
`- Compactions: ${t.compactions}, branches: ${t.branches}`,
|
||||
`- Top projects: ${topProjects}`,
|
||||
`- Tool usage: ${topTools}`,
|
||||
`- Peak hours: ${peakHours}`,
|
||||
"",
|
||||
"Full JSON (for reference):",
|
||||
"```json",
|
||||
JSON.stringify(stats),
|
||||
"```",
|
||||
].join("\n");
|
||||
|
||||
pi.sendUserMessage(prompt);
|
||||
ctx.ui.notify(
|
||||
`Analyzed ${t.sessions} sessions. Generating insights…`,
|
||||
"info"
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue