pi-tokenburn 0.3.0 → 0.3.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 CHANGED
@@ -86,9 +86,12 @@ Environment overrides for the session directory: `TOKENBURN_PI_SESSIONS`, `PI_CO
86
86
  ```bash
87
87
  bun install
88
88
  just check # typecheck + tests + pack check + nushell tests (what CI runs)
89
- just test # bun test only
89
+ just test # bun test only (with coverage)
90
+ just coverage # tests + a coverage table
90
91
  ```
91
92
 
93
+ **Coverage:** every `bun test` collects coverage (`bunfig.toml`) and **fails below 95% lines / 95% functions**. CI shows the table on the run page and uploads `lcov.info`.
94
+
92
95
  CI scripts are [nushell](https://www.nushell.sh) (`scripts/`), the same ones locally and in GitHub / Gitea Actions.
93
96
  `just --list` shows every task.
94
97
 
@@ -78,18 +78,24 @@ const DEFAULT_CONFIG: TokenBurnConfig = {
78
78
  budgets: {},
79
79
  };
80
80
 
81
- function getConfigDir(): string {
82
- if (process.env.PI_CODING_AGENT_DIR) return process.env.PI_CODING_AGENT_DIR;
83
- if (process.env.XDG_CONFIG_HOME) return join(process.env.XDG_CONFIG_HOME, "pi", "agent");
84
- return join(homedir(), ".pi", "agent");
81
+ /** Where pi keeps its config: PI_CODING_AGENT_DIR, then $XDG_CONFIG_HOME/pi/agent, then ~/.pi/agent. */
82
+ export function resolveAgentDir(env: Record<string, string | undefined>, home: string): string {
83
+ if (env.PI_CODING_AGENT_DIR) return env.PI_CODING_AGENT_DIR;
84
+ if (env.XDG_CONFIG_HOME) return join(env.XDG_CONFIG_HOME, "pi", "agent");
85
+ return join(home, ".pi", "agent");
85
86
  }
86
87
 
87
- function getSessionsDir(): string {
88
- if (process.env.TOKENBURN_PI_SESSIONS) return process.env.TOKENBURN_PI_SESSIONS;
89
- if (process.env.PI_CODING_AGENT_SESSION_DIR) return process.env.PI_CODING_AGENT_SESSION_DIR;
90
- return join(getConfigDir(), "sessions");
88
+ /** Pi session logs: TOKENBURN_PI_SESSIONS, PI_CODING_AGENT_SESSION_DIR, then <agent dir>/sessions. */
89
+ export function resolveSessionsDir(env: Record<string, string | undefined>, home: string): string {
90
+ if (env.TOKENBURN_PI_SESSIONS) return env.TOKENBURN_PI_SESSIONS;
91
+ if (env.PI_CODING_AGENT_SESSION_DIR) return env.PI_CODING_AGENT_SESSION_DIR;
92
+ return join(resolveAgentDir(env, home), "sessions");
91
93
  }
92
94
 
95
+ const getConfigDir = () => resolveAgentDir(process.env, homedir());
96
+
97
+ const getSessionsDir = () => resolveSessionsDir(process.env, homedir());
98
+
93
99
  export async function loadConfig(): Promise<TokenBurnConfig> {
94
100
  try {
95
101
  const raw = await readFile(join(getConfigDir(), "tokenburn.json"), "utf8");
@@ -172,6 +178,16 @@ async function parseFile(file: string): Promise<Turn[]> {
172
178
  return turns;
173
179
  }
174
180
 
181
+ /** Cache key for a file: mtime + size. `undefined` when it cannot be stat'ed. */
182
+ export function fileSignature(file: string): string | undefined {
183
+ try {
184
+ const st = statSync(file);
185
+ return `${st.mtimeMs}:${st.size}`;
186
+ } catch {
187
+ return undefined;
188
+ }
189
+ }
190
+
175
191
  function listSessionFiles(root: string): string[] {
176
192
  const files: string[] = [];
177
193
  const walk = (dir: string) => {
@@ -209,13 +225,8 @@ async function loadAll(sessionsRoot: string): Promise<Turn[][]> {
209
225
 
210
226
  const out: Turn[][] = [];
211
227
  for (const file of files) {
212
- let sig: string;
213
- try {
214
- const st = statSync(file);
215
- sig = `${st.mtimeMs}:${st.size}`;
216
- } catch {
217
- continue;
218
- }
228
+ const sig = fileSignature(file);
229
+ if (sig === undefined) continue; // vanished between listing and stat
219
230
  let entry = FILE_CACHE.get(file);
220
231
  if (!entry || entry.sig !== sig) {
221
232
  entry = { sig, turns: await parseFile(file) };
@@ -750,13 +761,14 @@ const TOP_LEVEL: CompletionItem[] = [
750
761
  /** `prefix` is the whole argument text typed so far (pi replaces it with `value`). */
751
762
  export function completions(prefix: string): CompletionItem[] | null {
752
763
  const text = (prefix ?? "").toLowerCase().replace(/^\s+/, "");
753
- const sub = (head: string, words: readonly string[], desc: (w: string) => string): CompletionItem[] =>
754
- words.map((w) => ({ value: `${head} ${w}`, label: w, description: desc(w) }));
764
+ // `tail` = " " when the completed text still needs another word (a budget amount).
765
+ const sub = (head: string, words: readonly string[], desc: (w: string) => string, tail = ""): CompletionItem[] =>
766
+ words.map((w) => ({ value: `${head} ${w}${tail}`, label: w, description: desc(w) }));
755
767
 
756
768
  let pool: CompletionItem[];
757
769
  if (text.startsWith("status ")) pool = sub("status", STATUS_WINDOWS, (w) => `Show ${w} in the status bar`);
758
770
  else if (text.startsWith("budget ") && !/^budget \S+ /.test(text))
759
- pool = sub("budget", BUDGET_PERIODS, (w) => `Set the ${w} budget (tokens or $cost)`);
771
+ pool = sub("budget", BUDGET_PERIODS, (w) => `Set the ${w} budget (tokens or $cost)`, " ");
760
772
  else if (/^(report|chart) /.test(text)) {
761
773
  const head = text.split(" ")[0];
762
774
  pool = sub(head, REPORT_VIEWS, (w) => `${head} for ${w}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-tokenburn",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Pi extension for live token usage monitoring and budget tracking (Day / Week / Month / Total). 0 prompt tokens.",
5
5
  "keywords": ["pi-package", "pi-extension", "tokenburn", "tokens", "analytics", "budget", "stats"],
6
6
  "license": "MIT",