opencode-brl-cost 0.1.2 → 0.1.3
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 +8 -5
- package/dist/index.js +109 -6
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
# opencode-brl-cost
|
|
2
2
|
|
|
3
|
-
OpenCode **TUI plugin** that pins
|
|
3
|
+
OpenCode **TUI plugin** that pins cost tracking to the bottom bar, in Brazilian Reais (R$).
|
|
4
4
|
|
|
5
5
|
## How it works
|
|
6
6
|
|
|
7
|
-
- Renders a fixed status bar via the `app_bottom` TUI slot
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- Renders a fixed status bar via the `app_bottom` TUI slot.
|
|
8
|
+
- Bottom-left shows **aggregated spend** since the start of today, this week (Monday), and this month.
|
|
9
|
+
- Bottom-right shows **the active session's total cost** (`session.cost`, USD).
|
|
10
|
+
- Costs come from each session's cumulative cost (`session.cost`, USD), attributed to the day it was **created**, converted with a **hardcoded rate of R$ 5,00 / US$ 1,00**.
|
|
11
|
+
- Sessions are enumerated via the SDK (`session.list`) on load and kept fresh through `session.*` events.
|
|
12
|
+
- Falls back to `R$ 0,00` when there's no data.
|
|
10
13
|
- Currency formatting uses `Intl.NumberFormat` with `pt-BR` (e.g. `R$ 1,23`).
|
|
11
14
|
|
|
12
15
|
## Install
|
|
@@ -19,7 +22,7 @@ Add the plugin to `tui.json`:
|
|
|
19
22
|
}
|
|
20
23
|
```
|
|
21
24
|
|
|
22
|
-
Then **restart OpenCode**. The bottom bar appears at the bottom
|
|
25
|
+
Then **restart OpenCode**. The bottom bar appears at the bottom.
|
|
23
26
|
|
|
24
27
|
> Pin a version for stability: `"opencode-brl-cost@0.1.2"`.
|
|
25
28
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// src/index.tsx
|
|
2
|
-
import {
|
|
2
|
+
import { createSignal } from "solid-js";
|
|
3
|
+
import { jsx, jsxs } from "@opentui/solid/jsx-runtime";
|
|
3
4
|
var BRL_PER_USD = 5;
|
|
4
5
|
var formatBRL = (usd) => {
|
|
5
6
|
const rounded = Math.round(usd * BRL_PER_USD * 100) / 100;
|
|
@@ -8,22 +9,124 @@ var formatBRL = (usd) => {
|
|
|
8
9
|
currency: "BRL"
|
|
9
10
|
}).format(rounded);
|
|
10
11
|
};
|
|
12
|
+
var startOfDay = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
|
|
13
|
+
var startOfWeek = (date) => {
|
|
14
|
+
const current = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
15
|
+
current.setDate(current.getDate() - (current.getDay() + 6) % 7);
|
|
16
|
+
return current.getTime();
|
|
17
|
+
};
|
|
18
|
+
var startOfMonth = (date) => new Date(date.getFullYear(), date.getMonth(), 1).getTime();
|
|
11
19
|
var tui = async (api) => {
|
|
20
|
+
const [dayCost, setDayCost] = createSignal("R$ 0,00");
|
|
21
|
+
const [weekCost, setWeekCost] = createSignal("R$ 0,00");
|
|
22
|
+
const [monthCost, setMonthCost] = createSignal("R$ 0,00");
|
|
23
|
+
const [sessionCost, setSessionCost] = createSignal("R$ 0,00");
|
|
24
|
+
const costs = /* @__PURE__ */ new Map();
|
|
25
|
+
const assess = (info) => {
|
|
26
|
+
if (!info || typeof info.cost !== "number" || typeof info.time?.created !== "number") return;
|
|
27
|
+
costs.set(info.id, { cost: info.cost, created: info.time.created });
|
|
28
|
+
};
|
|
12
29
|
const currentSessionID = () => {
|
|
13
30
|
const route = api.route.current;
|
|
14
31
|
return route.name === "session" && typeof route.params?.sessionID === "string" ? route.params.sessionID : void 0;
|
|
15
32
|
};
|
|
16
|
-
const
|
|
33
|
+
const fetchSessionCost = async (sessionID) => {
|
|
34
|
+
try {
|
|
35
|
+
const result = await api.client.session.get({ sessionID }, { throwOnError: true });
|
|
36
|
+
const info = result.data;
|
|
37
|
+
if (!info) return;
|
|
38
|
+
assess(info);
|
|
39
|
+
setSessionCost(formatBRL(typeof info.cost === "number" ? info.cost : 0));
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const refresh = () => {
|
|
44
|
+
const now = /* @__PURE__ */ new Date();
|
|
45
|
+
const dayStart = startOfDay(now);
|
|
46
|
+
const weekStart = startOfWeek(now);
|
|
47
|
+
const monthStart = startOfMonth(now);
|
|
48
|
+
let day = 0;
|
|
49
|
+
let week = 0;
|
|
50
|
+
let month = 0;
|
|
51
|
+
for (const entry of costs.values()) {
|
|
52
|
+
if (entry.created >= dayStart) day += entry.cost;
|
|
53
|
+
if (entry.created >= weekStart) week += entry.cost;
|
|
54
|
+
if (entry.created >= monthStart) month += entry.cost;
|
|
55
|
+
}
|
|
56
|
+
setDayCost(formatBRL(day));
|
|
57
|
+
setWeekCost(formatBRL(week));
|
|
58
|
+
setMonthCost(formatBRL(month));
|
|
17
59
|
const sessionID = currentSessionID();
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
60
|
+
const sessionCost2 = sessionID ? costs.get(sessionID)?.cost : void 0;
|
|
61
|
+
setSessionCost(formatBRL(typeof sessionCost2 === "number" ? sessionCost2 : 0));
|
|
62
|
+
if (sessionID) void fetchSessionCost(sessionID);
|
|
63
|
+
};
|
|
64
|
+
const seed = async () => {
|
|
65
|
+
try {
|
|
66
|
+
const result = await api.client.session.list({ limit: 1e3 });
|
|
67
|
+
if (Array.isArray(result?.data)) result.data.forEach((s) => assess(s));
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
refresh();
|
|
21
71
|
};
|
|
72
|
+
refresh();
|
|
73
|
+
void seed();
|
|
74
|
+
api.event.on("session.created", (event) => {
|
|
75
|
+
assess(event.properties.info);
|
|
76
|
+
refresh();
|
|
77
|
+
});
|
|
78
|
+
api.event.on("session.updated", (event) => {
|
|
79
|
+
assess(event.properties.info);
|
|
80
|
+
if (event.properties.sessionID === currentSessionID()) refresh();
|
|
81
|
+
});
|
|
82
|
+
api.event.on("session.deleted", (event) => {
|
|
83
|
+
costs.delete(event.properties.sessionID);
|
|
84
|
+
refresh();
|
|
85
|
+
});
|
|
86
|
+
api.event.on("session.idle", (event) => {
|
|
87
|
+
if (event.properties.sessionID === currentSessionID()) refresh();
|
|
88
|
+
});
|
|
89
|
+
api.event.on("tui.session.select", (event) => {
|
|
90
|
+
if (event.properties.sessionID === currentSessionID()) refresh();
|
|
91
|
+
});
|
|
22
92
|
api.slots.register({
|
|
23
93
|
slots: {
|
|
24
94
|
app_bottom(ctx) {
|
|
95
|
+
if (!currentSessionID()) return void 0;
|
|
25
96
|
const theme = ctx.theme.current;
|
|
26
|
-
|
|
97
|
+
const separator = " | ";
|
|
98
|
+
return /* @__PURE__ */ jsxs(
|
|
99
|
+
"box",
|
|
100
|
+
{
|
|
101
|
+
flexDirection: "row",
|
|
102
|
+
justifyContent: "space-between",
|
|
103
|
+
flexGrow: 1,
|
|
104
|
+
paddingLeft: 2,
|
|
105
|
+
paddingRight: 2,
|
|
106
|
+
border: true,
|
|
107
|
+
borderColor: theme.border,
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
|
|
110
|
+
/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: "dia: " }),
|
|
111
|
+
/* @__PURE__ */ jsx("text", { fg: theme.accent, children: dayCost() }),
|
|
112
|
+
/* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
|
|
113
|
+
separator,
|
|
114
|
+
"semana: "
|
|
115
|
+
] }),
|
|
116
|
+
/* @__PURE__ */ jsx("text", { fg: theme.success, children: weekCost() }),
|
|
117
|
+
/* @__PURE__ */ jsxs("text", { fg: theme.textMuted, children: [
|
|
118
|
+
separator,
|
|
119
|
+
"m\xEAs: "
|
|
120
|
+
] }),
|
|
121
|
+
/* @__PURE__ */ jsx("text", { fg: theme.warning, children: monthCost() })
|
|
122
|
+
] }),
|
|
123
|
+
/* @__PURE__ */ jsxs("box", { flexDirection: "row", children: [
|
|
124
|
+
/* @__PURE__ */ jsx("text", { fg: theme.textMuted, children: "\u25C6 session: " }),
|
|
125
|
+
/* @__PURE__ */ jsx("text", { fg: theme.text, children: sessionCost() })
|
|
126
|
+
] })
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
);
|
|
27
130
|
}
|
|
28
131
|
}
|
|
29
132
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/index.tsx"],
|
|
4
|
-
"sourcesContent": ["import
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { createSignal } from \"solid-js\"\nimport type { TuiPlugin, TuiPluginModule } from \"@opencode-ai/plugin/tui\"\n\nconst BRL_PER_USD = 5\n\nconst formatBRL = (usd: number) => {\n const rounded = Math.round(usd * BRL_PER_USD * 100) / 100\n return new Intl.NumberFormat(\"pt-BR\", {\n style: \"currency\",\n currency: \"BRL\",\n }).format(rounded)\n}\n\nconst startOfDay = (date: Date): number =>\n new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime()\n\nconst startOfWeek = (date: Date): number => {\n const current = new Date(date.getFullYear(), date.getMonth(), date.getDate())\n current.setDate(current.getDate() - ((current.getDay() + 6) % 7))\n return current.getTime()\n}\n\nconst startOfMonth = (date: Date): number =>\n new Date(date.getFullYear(), date.getMonth(), 1).getTime()\n\nconst tui: TuiPlugin = async (api) => {\n const [dayCost, setDayCost] = createSignal<string>(\"R$ 0,00\")\n const [weekCost, setWeekCost] = createSignal<string>(\"R$ 0,00\")\n const [monthCost, setMonthCost] = createSignal<string>(\"R$ 0,00\")\n const [sessionCost, setSessionCost] = createSignal<string>(\"R$ 0,00\")\n\n const costs = new Map<string, { cost: number; created: number }>()\n\n const assess = (info: { id: string; cost?: number; time?: { created?: number } } | undefined) => {\n if (!info || typeof info.cost !== \"number\" || typeof info.time?.created !== \"number\") return\n costs.set(info.id, { cost: info.cost, created: info.time.created })\n }\n\n const currentSessionID = (): string | undefined => {\n const route = api.route.current\n return route.name === \"session\" && typeof route.params?.sessionID === \"string\"\n ? route.params.sessionID\n : undefined\n }\n\n const fetchSessionCost = async (sessionID: string) => {\n try {\n const result = await api.client.session.get({ sessionID }, { throwOnError: true })\n const info = result.data\n if (!info) return\n assess(info)\n setSessionCost(formatBRL(typeof info.cost === \"number\" ? info.cost : 0))\n } catch {\n // ignore\n }\n }\n\n const refresh = () => {\n const now = new Date()\n const dayStart = startOfDay(now)\n const weekStart = startOfWeek(now)\n const monthStart = startOfMonth(now)\n let day = 0\n let week = 0\n let month = 0\n\n for (const entry of costs.values()) {\n if (entry.created >= dayStart) day += entry.cost\n if (entry.created >= weekStart) week += entry.cost\n if (entry.created >= monthStart) month += entry.cost\n }\n setDayCost(formatBRL(day))\n setWeekCost(formatBRL(week))\n setMonthCost(formatBRL(month))\n\n const sessionID = currentSessionID()\n const sessionCost = sessionID ? costs.get(sessionID)?.cost : undefined\n setSessionCost(formatBRL(typeof sessionCost === \"number\" ? sessionCost : 0))\n if (sessionID) void fetchSessionCost(sessionID)\n }\n\n const seed = async () => {\n try {\n const result = await api.client.session.list({ limit: 1000 })\n if (Array.isArray(result?.data)) result.data.forEach((s) => assess(s))\n } catch {\n // ignore\n }\n refresh()\n }\n\n refresh()\n void seed()\n\n api.event.on(\"session.created\", (event) => {\n assess(event.properties.info)\n refresh()\n })\n api.event.on(\"session.updated\", (event) => {\n assess(event.properties.info)\n if (event.properties.sessionID === currentSessionID()) refresh()\n })\n api.event.on(\"session.deleted\", (event) => {\n costs.delete(event.properties.sessionID)\n refresh()\n })\n api.event.on(\"session.idle\", (event) => {\n if (event.properties.sessionID === currentSessionID()) refresh()\n })\n api.event.on(\"tui.session.select\", (event) => {\n if (event.properties.sessionID === currentSessionID()) refresh()\n })\n\n api.slots.register({\n slots: {\n app_bottom(ctx) {\n if (!currentSessionID()) return undefined\n const theme = ctx.theme.current\n const separator = \" | \"\n return (\n <box\n flexDirection=\"row\"\n justifyContent=\"space-between\"\n flexGrow={1}\n paddingLeft={2}\n paddingRight={2}\n border\n borderColor={theme.border}\n >\n <box flexDirection=\"row\">\n <text fg={theme.textMuted}>dia: </text>\n <text fg={theme.accent}>{dayCost()}</text>\n <text fg={theme.textMuted}>{separator}semana: </text>\n <text fg={theme.success}>{weekCost()}</text>\n <text fg={theme.textMuted}>{separator}m\u00EAs: </text>\n <text fg={theme.warning}>{monthCost()}</text>\n </box>\n <box flexDirection=\"row\">\n <text fg={theme.textMuted}>\u25C6 session: </text>\n <text fg={theme.text}>{sessionCost()}</text>\n </box>\n </box>\n )\n },\n },\n })\n}\n\nconst plugin: TuiPluginModule = {\n id: \"opencode-brl-cost\",\n tui,\n}\n\nexport default plugin"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["sessionCost"]
|
|
7
7
|
}
|