vibeostheog 0.19.0 → 0.19.2

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,340 @@
1
+ import type { TuiPlugin } from "@opencode-ai/plugin/tui"
2
+ import { createSignal } from "solid-js"
3
+ import { existsSync, readFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { homedir } from "node:os"
6
+
7
+ const DEFAULT_PORT = 9578
8
+ const TIERS_FILE = join(homedir(), ".claude/model-tiers.json")
9
+
10
+ function getBaseUrl() {
11
+ try {
12
+ if (existsSync(TIERS_FILE)) {
13
+ const tiers = JSON.parse(readFileSync(TIERS_FILE, "utf-8"))
14
+ const port = Number(tiers?.selection?.mcp_port)
15
+ if (Number.isFinite(port) && port > 0) return `http://localhost:${port}`
16
+ }
17
+ } catch {}
18
+ return `http://localhost:${DEFAULT_PORT}`
19
+ }
20
+
21
+ type StatusResponse = {
22
+ todos: {
23
+ total: number
24
+ pending: number
25
+ }
26
+
27
+ enabled: boolean
28
+ active_slot: string
29
+ enforce: boolean
30
+ flow_enforcer: boolean
31
+ flow_extract_todos: boolean
32
+ tdd_enforcer: boolean
33
+ tdd_strict: boolean
34
+ thinking: string
35
+ current_model: string
36
+ credit_percent: number
37
+ version: string
38
+ backend_connected?: boolean
39
+ backend_health_url?: string | null
40
+ model_locked?: boolean
41
+ locked_slot?: string | null
42
+ locked_model?: string | null
43
+ }
44
+
45
+ type SavingsResponse = {
46
+ lifetime: {
47
+ delegation_usd: number
48
+ cache_usd: number
49
+ missed_context7_usd: number
50
+ total_warns: number
51
+ }
52
+ current_session: {
53
+ delegation_usd: number
54
+ cache_usd: number
55
+ warns_count: number
56
+ tool_breakdown: Record<string, number>
57
+ }
58
+ cache_hits_this_session: number
59
+ trend: string
60
+ savings_rate_per_hour: number
61
+ }
62
+
63
+ // Named export for TUI auto-discovery
64
+ export const vibeOSTui = async (api, _options, _meta) => {
65
+ try {
66
+ if (api?.ui?.toast) {
67
+ api.ui.toast({ variant: "info", message: "vibeOS TUI plugin executing" })
68
+ }
69
+ if (typeof process !== "undefined") {
70
+ process.stderr?.write?.("[vibeOS-tui] plugin function called\n")
71
+ }
72
+ } catch (e) {
73
+ if (typeof process !== "undefined") {
74
+ process.stderr?.write?.("[vibeOS-tui] ERROR: " + String(e) + "\n")
75
+ }
76
+ }
77
+ }
78
+
79
+ const plugin: TuiPlugin = async (api, _options, _meta) => {
80
+ const [status, setStatus] = createSignal<StatusResponse | null>(null)
81
+ const [savings, setSavings] = createSignal<SavingsResponse | null>(null)
82
+ const [error, setError] = createSignal<string | null>(null)
83
+
84
+ const poll = async () => {
85
+ try {
86
+ const baseUrl = getBaseUrl()
87
+ const [s, sa] = await Promise.all([
88
+ fetch(`${baseUrl}/status`).then((r) => r.json()),
89
+ fetch(`${baseUrl}/savings`).then((r) => r.json()),
90
+ ])
91
+ setStatus(s)
92
+ setSavings(sa)
93
+ setError(null)
94
+ } catch {
95
+ setError("vibeOS MCP offline")
96
+ }
97
+ }
98
+
99
+ await poll()
100
+ const timer = setInterval(poll, 3000)
101
+
102
+ api.lifecycle.onDispose(() => {
103
+ clearInterval(timer)
104
+ })
105
+
106
+ const doAction = async (body: Record<string, unknown>) => {
107
+ try {
108
+ const res = await fetch(`${getBaseUrl()}/trinity`, {
109
+ method: "POST",
110
+ headers: { "Content-Type": "application/json" },
111
+ body: JSON.stringify(body),
112
+ })
113
+ const data = await res.json()
114
+ if (data.ok) {
115
+ api.ui.toast({ variant: "success", message: data.result })
116
+ await poll()
117
+ } else {
118
+ api.ui.toast({ variant: "error", message: "Action failed" })
119
+ }
120
+ } catch {
121
+ api.ui.toast({ variant: "error", message: "vibeOS offline" })
122
+ }
123
+ }
124
+
125
+ const Slot = api.ui.Slot
126
+
127
+ api.slots.register((props: { session_id: string }) => {
128
+ const s = status()
129
+ const sv = savings()
130
+
131
+ if (error()) {
132
+ return (
133
+ <box flexDirection="column">
134
+ <Slot name="sidebar_title" session_id={props.session_id} title="vibeOS">
135
+ <text dim>vibeOS offline</text>
136
+ </Slot>
137
+ <Slot name="sidebar_content" session_id={props.session_id}>
138
+ <box flexDirection="column" padding={1}>
139
+ <text dim>vibeOS MCP not running</text>
140
+ <newline />
141
+ <text dim>ensure the server plugin is active</text>
142
+ </box>
143
+ </Slot>
144
+ <Slot name="sidebar_footer" session_id={props.session_id}>
145
+ <text dim>vibeOS MCP offline</text>
146
+ </Slot>
147
+ </box>
148
+ )
149
+ }
150
+
151
+ const activeSlot = s?.active_slot ?? "?"
152
+ const enabled = s?.enabled ?? false
153
+ const trendArrow = sv?.trend === "up" ? "^" : sv?.trend === "down" ? "v" : "-"
154
+ const delegation = (sv?.current_session?.delegation_usd ?? 0) + (sv?.lifetime?.delegation_usd ?? 0)
155
+ const cache = (sv?.current_session?.cache_usd ?? 0) + (sv?.lifetime?.cache_usd ?? 0)
156
+ const lifetime = (sv?.lifetime?.delegation_usd ?? 0) + (sv?.lifetime?.cache_usd ?? 0)
157
+ const missedC7 = sv?.lifetime?.missed_context7_usd ?? 0
158
+ const toolBreakdown = sv?.current_session?.tool_breakdown ?? {}
159
+ const topTools = Object.entries(toolBreakdown)
160
+ .sort(([, a], [, b]) => (b as number) - (a as number))
161
+ .slice(0, 5)
162
+ const trendColor = sv?.trend === "up" ? "green" : sv?.trend === "down" ? "red" : "yellow"
163
+ const shortModel = s?.current_model?.split("/")[1] ?? s?.current_model ?? "?"
164
+ const flowOn = s?.flow_enforcer ?? false
165
+ const tddOn = s?.tdd_enforcer ?? false
166
+ const backendConnected = s?.backend_connected ?? false
167
+ const lockLabel = s?.model_locked
168
+ ? `${s?.locked_slot ? `${s.locked_slot} ` : ""}${s?.locked_model ?? ""}`.trim()
169
+ : "off"
170
+
171
+ return (
172
+ <box flexDirection="column">
173
+ <Slot name="sidebar_title" session_id={props.session_id} title="vibeOS">
174
+ <box>
175
+ <text bold>vibeOS</text>
176
+ <text dim> | </text>
177
+ <text color={enabled ? "green" : "red"} bold>{activeSlot}</text>
178
+ <text> </text>
179
+ <text color={enabled ? "green" : "red"}>{enabled ? "." : "o"}</text>
180
+ </box>
181
+ </Slot>
182
+ <Slot name="sidebar_content" session_id={props.session_id}>
183
+ <box flexDirection="column" padding={1}>
184
+ <text dim bold>MODEL STATUS</text>
185
+ <newline />
186
+ <box>
187
+ <text bold={activeSlot === "brain"} color={activeSlot === "brain" ? "green" : undefined}>
188
+ {shortModel}
189
+ </text>
190
+ {activeSlot === "brain" && <text color="green"> active</text>}
191
+ </box>
192
+ <newline />
193
+ <box>
194
+ <text>Backend </text>
195
+ <text color={backendConnected ? "green" : "red"} bold>{backendConnected ? "ON" : "OFF"}</text>
196
+ </box>
197
+ <newline />
198
+ <box>
199
+ <text>Lock </text>
200
+ <text color={s?.model_locked ? "green" : "red"} bold>{s?.model_locked ? "ON" : "OFF"}</text>
201
+ {s?.model_locked && lockLabel && <text dim> {lockLabel}</text>}
202
+ </box>
203
+ <newline />
204
+ <text dim>---</text>
205
+ <newline />
206
+ <box>
207
+ <text>Flow </text>
208
+ <text color={flowOn ? "green" : "red"} bold>{flowOn ? "ON" : "OFF"}</text>
209
+ </box>
210
+ <newline />
211
+ <box>
212
+ <text>TDD </text>
213
+ <text color={tddOn ? "green" : "red"} bold>{tddOn ? "ON" : "OFF"}</text>
214
+ {tddOn && s?.tdd_strict && <text dim> strict</text>}
215
+ </box>
216
+ <newline />
217
+ <box>
218
+ <text>Enforce </text>
219
+ <text color={s?.enforce ? "green" : "red"} bold>{s?.enforce ? "ON" : "OFF"}</text>
220
+ </box>
221
+ <newline />
222
+ <text dim>---</text>
223
+ <newline />
224
+ <box>
225
+ <text>Thinking: </text>
226
+ <text>{s?.thinking ?? "?"}</text>
227
+ </box>
228
+ <newline />
229
+ <newline />
230
+ <text dim bold>SAVINGS</text>
231
+ <newline />
232
+ <box>
233
+ <text bold>Saved: </text>
234
+ <text bold color={trendColor}>${lifetime.toFixed(2)} {trendArrow}</text>
235
+ </box>
236
+ <newline />
237
+ <box><text> Delegation: </text><text>${delegation.toFixed(2)}</text></box>
238
+ <newline />
239
+ <box><text> Cache: </text><text>${cache.toFixed(2)}</text></box>
240
+ <newline />
241
+ <box><text> C7 missed: </text><text>${missedC7.toFixed(2)}</text></box>
242
+ <newline />
243
+ <text dim>---</text>
244
+ <newline />
245
+ <box><text>Rate: </text><text>${sv?.savings_rate_per_hour?.toFixed(2) ?? "0.00"}/hr</text></box>
246
+ <newline />
247
+ <box><text>Warns: </text><text>{sv?.current_session?.warns_count ?? 0}</text></box>
248
+ <newline />
249
+ <text dim>---</text>
250
+ <newline />
251
+ <text dim>Tool split:</text>
252
+ <newline />
253
+ {topTools.map(([tool, val]) => (
254
+ <newline />
255
+ <text dim bold>TODOS</text>
256
+ <newline />
257
+ <box>
258
+ <text>Pending: </text>
259
+ <text color={s?.todos?.pending > 0 ? "yellow" : "green"} bold>
260
+ {s?.todos?.pending ?? 0}
261
+ </text>
262
+ <text> / {s?.todos?.total ?? 0}</text>
263
+ </box>
264
+ <newline />
265
+
266
+ <>
267
+ <box>
268
+ <text> {tool.padEnd(8)}</text>
269
+ <text>${(val as number).toFixed(2)}</text>
270
+ </box>
271
+ <newline />
272
+ </>
273
+ ))}
274
+ <newline />
275
+ <text dim bold>CONTROLS</text>
276
+ <newline />
277
+ <box>
278
+ <text
279
+ onClick={() => doAction({ action: "set", slot: "brain", level: null })}
280
+ color={activeSlot === "brain" ? "green" : "dim"}
281
+ bold={activeSlot === "brain"}
282
+ >[brain]</text>
283
+ <text> </text>
284
+ <text
285
+ onClick={() => doAction({ action: "set", slot: "medium", level: null })}
286
+ color={activeSlot === "medium" ? "green" : "dim"}
287
+ bold={activeSlot === "medium"}
288
+ >[medium]</text>
289
+ <text> </text>
290
+ <text
291
+ onClick={() => doAction({ action: "set", slot: "cheap", level: null })}
292
+ color={activeSlot === "cheap" ? "green" : "dim"}
293
+ bold={activeSlot === "cheap"}
294
+ >[cheap]</text>
295
+ </box>
296
+ <newline />
297
+ <box>
298
+ <text
299
+ onClick={() => doAction({ action: "flow", slot: flowOn ? "off" : "on", level: null })}
300
+ color={flowOn ? "green" : "red"} bold
301
+ >[Flow {flowOn ? "ON" : "OFF"}]</text>
302
+ </box>
303
+ <newline />
304
+ <box>
305
+ <text
306
+ onClick={() => doAction({ action: "tdd", slot: tddOn ? "off" : "on", level: null })}
307
+ color={tddOn ? "green" : "red"} bold
308
+ >[TDD {tddOn ? "ON" : "OFF"}]</text>
309
+ </box>
310
+ <newline />
311
+ <box>
312
+ <text
313
+ onClick={() => doAction({ action: "enforce", slot: s?.enforce ? "off" : "on", level: null })}
314
+ color={s?.enforce ? "green" : "red"} bold
315
+ >[Enforce {s?.enforce ? "ON" : "OFF"}]</text>
316
+ </box>
317
+ <newline />
318
+ <box>
319
+ <text
320
+ onClick={() => doAction({ action: "disable", slot: null, level: null })}
321
+ color="red" bold
322
+ >[Disable]</text>
323
+ </box>
324
+ </box>
325
+ </Slot>
326
+ <Slot name="sidebar_footer" session_id={props.session_id}>
327
+ <box>
328
+ <text dim>Saved </text>
329
+ <text color={trendColor}>{lifetime.toFixed(2)}</text>
330
+ <text dim> {trendArrow} </text>
331
+ <text>{sv?.savings_rate_per_hour?.toFixed(2) ?? "0.00"}/hr</text>
332
+ <text dim> | {sv?.current_session?.warns_count ?? 0} warns</text>
333
+ </box>
334
+ </Slot>
335
+ </box>
336
+ )
337
+ })
338
+ }
339
+
340
+ export default { tui: plugin }
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## 0.19.2
2
+ - fix: export server and tui entrypoints
3
+
4
+ ## 0.19.1
5
+ - fix: make README and runtime self-contained
6
+ - test: add 59 integration + e2e tests for cross-module behavior and user workflows
7
+ Merge pull request #49 from DrunkkToys/oc-desktop-live-savings-refresh
8
+ Merge pull request #48 from DrunkkToys/codex/live-savings-refresh
9
+ Invalidate savings cache on state writes
10
+
11
+
1
12
  ## 0.19.0
2
13
  - feat: quality governance — self-protection, 2h release gate, outcome tracking (#44)
3
14
  - fix: chooseEpisodeMode defaults to budget (was always quality) + simplify isApiConnected check
package/README.md CHANGED
@@ -21,7 +21,7 @@ It also adds guardrails: delegation enforcement, flow and TDD controls, pattern
21
21
  1. Install the package:
22
22
 
23
23
  ```bash
24
- npm install vibeOS
24
+ npm install vibeostheog
25
25
  ```
26
26
 
27
27
  2. Register it in `~/.config/opencode/opencode.json`:
@@ -29,41 +29,39 @@ npm install vibeOS
29
29
  ```json
30
30
  {
31
31
  "plugins": [
32
- { "id": "vibeOS", "path": "node_modules/vibeOS/src/index.js" }
32
+ { "id": "vibeOS", "path": "node_modules/vibeostheog/src/index.js" }
33
33
  ]
34
34
  }
35
35
  ```
36
36
 
37
37
  ### Local plugin file
38
38
 
39
- If you keep a local copy of the plugin, point OpenCode at the built file instead:
39
+ If you keep a local checkout of the plugin, point OpenCode at the built file instead:
40
40
 
41
41
  ```json
42
42
  {
43
43
  "plugins": [
44
- { "id": "vibeOS", "path": "~/.config/opencode/plugins/vibeOS.js" }
44
+ { "id": "vibeOS", "path": "/absolute/path/to/theSaver-oc/src/index.js" }
45
45
  ]
46
46
  }
47
47
  ```
48
48
 
49
49
  Restart OpenCode Desktop after changing the config.
50
50
 
51
- ## Backend Root
51
+ The package also exposes `vibeostheog/server` and `vibeostheog/tui` for integrations that need the MCP server or sidebar plugin entrypoints directly.
52
52
 
53
- The backend lives at the Desktop-level sibling folder `../vibeOScore`.
54
-
55
- Use that folder when you need the API server, MCP server, or dashboard build chain:
53
+ ## Common Npm Commands
56
54
 
57
55
  ```bash
58
- npm run start
59
- npm run start:all
60
- npm run build:dashboard
61
- npm run dev:dashboard
62
- npm run dashboard:serve
56
+ npm install
57
+ npm run build
63
58
  npm run typecheck
64
59
  npm test
60
+ npm run release:patch
65
61
  ```
66
62
 
63
+ `npm run build` compiles `src/index.ts` to `src/index.js` and deploys the built plugin into the OpenCode plugin directory. `npm run typecheck` validates the TypeScript sources without emitting files.
64
+
67
65
  ## Core Controls
68
66
 
69
67
  Use `trinity help` for the full command list. The most common controls are:
@@ -127,11 +125,10 @@ Without a token, vibeOS keeps running in local-only mode with bundled algorithms
127
125
  - If writes or edits are blocked, that is usually delegation enforcement working as intended on the brain tier.
128
126
  - If the footer is missing, check that the plugin is enabled and that the current OpenCode session is receiving assistant completions.
129
127
  - If the remote API is down or the token is invalid, use `trinity api-token <token>` or rely on local-only mode.
130
- - If the dashboard does not load, rebuild it from the backend root with `npm run build:dashboard`.
128
+ - If the dashboard does not load, rebuild the plugin with `npm run build` and restart OpenCode.
131
129
  - If state or config looks inconsistent, run `trinity diagnose` and `trinity guard`.
132
130
 
133
131
  ## Notes
134
132
 
135
133
  - `trinity help` is the canonical command reference.
136
- - `../vibeOScore` is the backend sibling directory on the Desktop.
137
134
  - The README stays intentionally high level so the command details can follow the code without a rewrite.
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "vibeostheog",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Cost-aware delegation enforcer for OpenCode. Tracks model usage, routes Task subagents to cheaper tiers, surfaces cumulative savings in chat. Includes research audit, reporting framework, project memory, progressive scratchpad decadence, and trinity CLI for brain/medium/cheap slot switching.",
5
5
  "scripts": {
6
6
  "release": "node scripts/release.mjs",
7
7
  "release:patch": "node scripts/release.mjs patch --yes",
8
8
  "release:minor": "node scripts/release.mjs minor --yes",
9
9
  "release:major": "node scripts/release.mjs major --yes",
10
- "build": "tsc -p tsconfig.json --noEmit && npx esbuild src/index.ts --bundle --outfile=src/index.js --platform=node --format=esm --target=node22 --external:node:* --external:vibeOScore && node scripts/deploy.mjs",
10
+ "build": "tsc -p tsconfig.json --noEmit && npx esbuild src/index.ts --bundle --outfile=src/index.js --platform=node --format=esm --target=node22 --external:node:* && node scripts/deploy.mjs",
11
11
  "deploy": "node scripts/deploy.mjs",
12
12
  "typecheck": "tsc -p tsconfig.json --noEmit",
13
13
  "checkpoint:validate": "node scripts/checkpoint-validate.mjs",
@@ -26,7 +26,9 @@
26
26
  },
27
27
  "type": "module",
28
28
  "exports": {
29
- ".": "./src/index.js"
29
+ ".": "./src/index.js",
30
+ "./server": "./src/lib/vibeos-mcp-server.js",
31
+ "./tui": "./.opencode/plugins/vibeOS-tui.tsx"
30
32
  },
31
33
  "keywords": [
32
34
  "opencode",
@@ -46,6 +48,8 @@
46
48
  },
47
49
  "files": [
48
50
  "src/index.js",
51
+ "src/lib/vibeos-mcp-server.js",
52
+ ".opencode/plugins/vibeOS-tui.tsx",
49
53
  "scripts/deploy.mjs",
50
54
  "model-tiers.sample.json",
51
55
  "README.md",
@@ -66,8 +70,7 @@
66
70
  "@types/node": "^22.15.30",
67
71
  "esbuild": "^0.28.0",
68
72
  "express": "^5.2.1",
69
- "typescript": "^5.9.3",
70
- "vibeOScore": "file:../vibeOScore"
73
+ "typescript": "^5.9.3"
71
74
  },
72
75
  "dependencies": {}
73
76
  }