@shanepadgett/tau-agent 0.39.2 → 0.40.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.
@@ -0,0 +1,64 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { getKeybindings, type Component, type TUI, truncateToWidth } from "@earendil-works/pi-tui";
3
+ import { bindingHint, ToolPanel, type ToolPanelConfig } from "@shanepadgett/tau-tui";
4
+
5
+ export class CostReportStatusPanel implements Component {
6
+ private readonly tui: TUI;
7
+ private readonly panel: ToolPanel;
8
+ private readonly config: ToolPanelConfig;
9
+ private readonly controller = new AbortController();
10
+ private readonly startedAt = Date.now();
11
+ private readonly timer: ReturnType<typeof setInterval>;
12
+ private status = "Starting…";
13
+ private readonly secondary: string;
14
+
15
+ constructor(tui: TUI, theme: Theme, secondary: string) {
16
+ this.tui = tui;
17
+ this.secondary = secondary;
18
+ this.config = {
19
+ title: "Cost report",
20
+ secondary,
21
+ body: {
22
+ render: (width) => [truncateToWidth(theme.fg("muted", this.status), width, "…")],
23
+ invalidate: () => {},
24
+ },
25
+ footer: { kind: "hints", hints: [bindingHint("tui.select.cancel", "cancel")] },
26
+ border: "box",
27
+ };
28
+ this.panel = new ToolPanel(theme, this.config);
29
+ this.timer = setInterval(() => this.tui.requestRender(), 1_000);
30
+ this.timer.unref();
31
+ }
32
+
33
+ get signal(): AbortSignal {
34
+ return this.controller.signal;
35
+ }
36
+
37
+ handleInput(data: string): void {
38
+ if (!getKeybindings().matches(data, "tui.select.cancel")) return;
39
+ if (this.controller.signal.aborted) return;
40
+ this.update("Cancelling…");
41
+ this.controller.abort();
42
+ }
43
+
44
+ update(status: string): void {
45
+ this.status = status.trim() || this.status;
46
+ this.tui.requestRender();
47
+ }
48
+
49
+ render(width: number): string[] {
50
+ const elapsed = Math.floor((Date.now() - this.startedAt) / 1_000);
51
+ const clock = elapsed < 60 ? `${elapsed}s` : `${Math.floor(elapsed / 60)}m ${elapsed % 60}s`;
52
+ this.config.title = `Cost report · ${clock}`;
53
+ this.config.secondary = this.secondary;
54
+ return this.panel.render(width);
55
+ }
56
+
57
+ invalidate(): void {
58
+ this.panel.invalidate();
59
+ }
60
+
61
+ dispose(): void {
62
+ clearInterval(this.timer);
63
+ }
64
+ }
@@ -0,0 +1,145 @@
1
+ import type { LiveRangeKind, ReportRange } from "./types.ts";
2
+
3
+ const MONTH_LABELS = [
4
+ "January",
5
+ "February",
6
+ "March",
7
+ "April",
8
+ "May",
9
+ "June",
10
+ "July",
11
+ "August",
12
+ "September",
13
+ "October",
14
+ "November",
15
+ "December",
16
+ ] as const;
17
+
18
+ export const LIVE_RANGE_CHOICES: ReadonlyArray<{ id: LiveRangeKind | "specific-month"; label: string }> = [
19
+ { id: "past-7-days", label: "Past 7 days" },
20
+ { id: "current-week", label: "Current week" },
21
+ { id: "current-month", label: "Current month" },
22
+ { id: "year-to-date", label: "Year to date" },
23
+ { id: "specific-month", label: "Specific month…" },
24
+ ];
25
+
26
+ export const MONTH_CHOICES: ReadonlyArray<{ month: number; label: string }> = MONTH_LABELS.map((label, month) => ({
27
+ month,
28
+ label,
29
+ }));
30
+
31
+ export function yearChoices(now = new Date()): number[] {
32
+ const current = now.getFullYear();
33
+ return Array.from({ length: 6 }, (_, index) => current - index);
34
+ }
35
+
36
+ export function liveRange(kind: LiveRangeKind, now = new Date()): ReportRange {
37
+ const endMs = now.getTime();
38
+ if (kind === "past-7-days") {
39
+ const start = startOfLocalDay(now);
40
+ start.setDate(start.getDate() - 6);
41
+ // Slides daily; slug pins the window end day so same-day reopens match.
42
+ return {
43
+ kind,
44
+ label: "Past 7 days",
45
+ slug: `7d-${localDateKey(endMs)}`,
46
+ startMs: start.getTime(),
47
+ endMs,
48
+ layout: "row",
49
+ };
50
+ }
51
+ if (kind === "current-week") {
52
+ const start = startOfLocalDay(now);
53
+ const day = start.getDay(); // 0 Sun … 6 Sat
54
+ const mondayOffset = day === 0 ? -6 : 1 - day;
55
+ start.setDate(start.getDate() + mondayOffset);
56
+ return {
57
+ kind,
58
+ label: "Current week",
59
+ slug: `week-${localDateKey(start.getTime())}`,
60
+ startMs: start.getTime(),
61
+ endMs,
62
+ layout: "row",
63
+ };
64
+ }
65
+ if (kind === "current-month") {
66
+ const start = new Date(now.getFullYear(), now.getMonth(), 1);
67
+ const ym = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
68
+ return {
69
+ kind,
70
+ label: "Current month",
71
+ slug: `month-${ym}`,
72
+ startMs: start.getTime(),
73
+ endMs,
74
+ layout: "row",
75
+ year: now.getFullYear(),
76
+ month: now.getMonth(),
77
+ };
78
+ }
79
+ const start = new Date(now.getFullYear(), 0, 1);
80
+ return {
81
+ kind,
82
+ label: "Year to date",
83
+ slug: `ytd-${now.getFullYear()}`,
84
+ startMs: start.getTime(),
85
+ endMs,
86
+ layout: "ytd",
87
+ year: now.getFullYear(),
88
+ };
89
+ }
90
+
91
+ export function specificMonthRange(year: number, month: number, now = new Date()): ReportRange {
92
+ const start = new Date(year, month, 1);
93
+ const endExclusive = new Date(year, month + 1, 1);
94
+ const endMs = Math.min(now.getTime(), endExclusive.getTime() - 1);
95
+ const monthLabel = MONTH_LABELS[month] ?? `Month ${month + 1}`;
96
+ const ym = `${year}-${String(month + 1).padStart(2, "0")}`;
97
+ return {
98
+ kind: "specific-month",
99
+ label: `${monthLabel} ${year}`,
100
+ slug: ym,
101
+ startMs: start.getTime(),
102
+ endMs,
103
+ layout: "row",
104
+ year,
105
+ month,
106
+ };
107
+ }
108
+
109
+ export function startOfLocalDay(date: Date): Date {
110
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate());
111
+ }
112
+
113
+ export function localDateKey(timestampMs: number): string {
114
+ const date = new Date(timestampMs);
115
+ const y = date.getFullYear();
116
+ const m = String(date.getMonth() + 1).padStart(2, "0");
117
+ const d = String(date.getDate()).padStart(2, "0");
118
+ return `${y}-${m}-${d}`;
119
+ }
120
+
121
+ export function shortDateLabel(timestampMs: number): string {
122
+ const date = new Date(timestampMs);
123
+ const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
124
+ return `${date.getDate()} ${months[date.getMonth()]}`;
125
+ }
126
+
127
+ export function isWeekend(timestampMs: number): boolean {
128
+ const day = new Date(timestampMs).getDay();
129
+ return day === 0 || day === 6;
130
+ }
131
+
132
+ export function eachLocalDay(startMs: number, endMs: number): number[] {
133
+ const days: number[] = [];
134
+ const cursor = startOfLocalDay(new Date(startMs));
135
+ const end = startOfLocalDay(new Date(endMs));
136
+ while (cursor.getTime() <= end.getTime()) {
137
+ days.push(cursor.getTime());
138
+ cursor.setDate(cursor.getDate() + 1);
139
+ }
140
+ return days;
141
+ }
142
+
143
+ export function daysInMonth(year: number, month: number): number {
144
+ return new Date(year, month + 1, 0).getDate();
145
+ }
@@ -0,0 +1,93 @@
1
+ export type ReportScope = "project" | "all";
2
+
3
+ export type LiveRangeKind = "past-7-days" | "current-week" | "current-month" | "year-to-date";
4
+
5
+ export type RangeKind = LiveRangeKind | "specific-month";
6
+
7
+ export interface ReportRange {
8
+ kind: RangeKind;
9
+ label: string;
10
+ slug: string;
11
+ startMs: number;
12
+ endMs: number;
13
+ /** Single full-width day row vs one row per month (YTD). */
14
+ layout: "row" | "ytd";
15
+ year?: number;
16
+ month?: number; // 0-11
17
+ }
18
+
19
+ export interface CostTotals {
20
+ input: number;
21
+ output: number;
22
+ cacheRead: number;
23
+ cacheWrite: number;
24
+ totalTokens: number;
25
+ cost: number;
26
+ }
27
+
28
+ export interface ModelCost {
29
+ key: string;
30
+ provider: string;
31
+ model: string;
32
+ cost: number;
33
+ tokens: number;
34
+ sessions: number;
35
+ }
36
+
37
+ export interface SubagentCost {
38
+ agent: string;
39
+ cost: number;
40
+ calls: number;
41
+ }
42
+
43
+ export interface ProjectCost {
44
+ key: string;
45
+ label: string;
46
+ cost: number;
47
+ sessions: number;
48
+ tokens: number;
49
+ }
50
+
51
+ export interface SessionModelCost {
52
+ key: string;
53
+ label: string;
54
+ cost: number;
55
+ }
56
+
57
+ export interface SessionCost {
58
+ id: string;
59
+ path: string;
60
+ name: string;
61
+ projectKey: string;
62
+ projectLabel: string;
63
+ startedAtMs: number;
64
+ cost: number;
65
+ tokens: number;
66
+ models: SessionModelCost[];
67
+ }
68
+
69
+ export interface DayCost {
70
+ dateKey: string; // YYYY-MM-DD local
71
+ label: string;
72
+ timestampMs: number;
73
+ cost: number;
74
+ weekend: boolean;
75
+ }
76
+
77
+ export interface CostReport {
78
+ generatedAtMs: number;
79
+ cwd: string;
80
+ scope: ReportScope;
81
+ range: ReportRange;
82
+ directCost: number;
83
+ subagentCost: number;
84
+ totalCost: number;
85
+ totalTokens: number;
86
+ sessionCount: number;
87
+ projectCount: number;
88
+ days: DayCost[];
89
+ models: ModelCost[];
90
+ subagents: SubagentCost[];
91
+ projects: ProjectCost[];
92
+ sessions: SessionCost[];
93
+ }
@@ -40,6 +40,10 @@ Adds `/clear-screen` to clear terminal output without changing the session.
40
40
 
41
41
  Adds `/commit` for semantic commit grouping, review, and committing selected repository changes.
42
42
 
43
+ ## cost-report
44
+
45
+ Adds `/cost-report` to build an HTML spend report from local session usage. Pick a time frame (past 7 days, current week, current month, year to date, or a specific month) and scope (current project or all sessions). Tau scans sessions, writes under `~/.pi/tau/cost-reports/`, opens the file, and notifies with the path. Empty windows warn without writing a file.
46
+
43
47
  ## context
44
48
 
45
49
  Adds `/context` to inject reusable repository work scopes from `.pi/contexts`, and `/context-sync` or `/context-sync <nudge>` for human-driven catalog sync. Selecting entries injects them once into the conversation: `read` paths as complete files, `show` targets as current declaration slices, `outline` paths as Explore structures, and one hidden note listing `references` plus instructions to treat the injected material as current. Run `/context` again to inject more. Manual sync replaces the editor with a status panel; Escape or Ctrl+C cancels. When `sync.automation` is on, coding agent can also run `context-sync` after meaningful uncommitted work. Sync catalogs durable code and long-lived documentation; recurring scratch, planning, interview, and rough-idea paths belong in `validation.ignoreGlobs`. `sync.enabled` is master switch for command, automation, and validation auto-run. Context validation is off by default; when on (and sync enabled), Tau auto-runs context-sync on failure. Domain folders are `NN_slug` tabs (ordered by the two-digit prefix; UI shows the slug), TOML files are concepts, and TOML sections are selectable entries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shanepadgett/tau-agent",
3
- "version": "0.39.2",
3
+ "version": "0.40.0",
4
4
  "description": "Tau is a custom agentic harness built with pi extensions",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -35,7 +35,7 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@ast-grep/wasm": "0.45.1",
38
- "@shanepadgett/tau-tui": "0.39.2",
38
+ "@shanepadgett/tau-tui": "0.40.0",
39
39
  "@vscode/tree-sitter-wasm": "0.3.1",
40
40
  "image-size": "2.0.2",
41
41
  "smol-toml": "1.8.0",