opencube 0.3.3 → 0.4.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
@@ -4,152 +4,64 @@
4
4
 
5
5
  # OpenCube
6
6
 
7
- OpenCube is a tiny desktop pet for [opencode](https://opencode.ai/).
7
+ A tiny desktop cube pet for [opencode](https://opencode.ai/).
8
8
 
9
- It watches opencode session activity and renders a small Three.js cube on your desktop:
10
-
11
- - busy sessions light up cube faces
12
- - active tool calls emit bright face-colored sparks from the corresponding cube face
13
- - right-click drag applies extra rotational friction with small braking particles
14
- - idle sessions keep a subtle torque-breathing motion and release face glow after the cube slows down
15
- - a macOS-style tray/menu bar control can show, hide, quit, or open the Inbox
16
- - the Inbox shows raw opencode hook events plus local mouse/keyboard diagnostics
17
- - `/pet_say_hello` flashes one free face
18
- - `/pet_fancy_say_hello` plays a randomized light show on free faces
19
-
20
- OpenCube is packaged as an opencode plugin plus an Electron desktop process.
9
+ It lights up for busy sessions, tool calls, permissions, and questions.
21
10
 
22
11
  ## Install
23
12
 
24
- Install the latest published version globally through opencode:
25
-
26
13
  ```sh
27
14
  opencode plugin opencube --global
28
15
  ```
29
16
 
30
- Then restart opencode and run:
17
+ Restart opencode, then run:
31
18
 
32
19
  ```text
33
20
  /pet
34
21
  ```
35
22
 
36
- You can also add it manually to `~/.config/opencode/opencode.json`:
37
-
38
- ```json
39
- {
40
- "plugin": ["opencube"]
41
- }
42
- ```
43
-
44
23
  ## Update
45
24
 
46
- If you already installed OpenCube and want to upgrade, reinstall the target version with `--force`:
47
-
48
25
  ```sh
49
- opencode plugin opencube@0.3.1 --global --force
26
+ opencode plugin opencube@0.4.0 --global --force
50
27
  ```
51
28
 
52
- Using an explicit version is recommended for upgrades because it avoids stale `latest` cache behavior. You can still install the npm latest tag if desired:
29
+ Then restart opencode and run `/pet` again.
53
30
 
54
- ```sh
55
- opencode plugin opencube@latest --global --force
56
- ```
57
-
58
- Then fully restart opencode and run `/pet` again. OpenCube and opencode plugins are loaded at startup, so the running desktop pet is not hot-replaced in place.
59
-
60
- You can ask OpenCube to check npm for the latest published version:
31
+ You can also ask OpenCube for the latest published version:
61
32
 
62
33
  ```text
63
34
  /pet_update
64
35
  ```
65
36
 
66
- `/pet_update` does not hot-replace the running plugin. It reports whether a newer version exists and prints an explicit `opencode plugin opencube@<version> --global --force` command to run.
67
-
68
37
  ## Commands
69
38
 
70
- | Command | Description |
71
- | --- | --- |
72
- | `/pet` | Show or start OpenCube. |
73
- | `/pet_stop` | Quit OpenCube. |
74
- | `/pet_say_hello` | Flash one currently free face three times with a random color. |
75
- | `/pet_fancy_say_hello` | Run a denser randomized light show across currently free faces. |
76
- | `/pet_update` | Check npm for a newer OpenCube release and show the upgrade command. |
77
- | `/pet_upgrade` | Alias for `/pet_update`. |
78
-
79
- These commands are handled by the plugin and do not get sent to the model.
80
-
81
- ## What you can see
82
-
83
- OpenCube turns opencode lifecycle events into small desktop signals:
84
-
85
- | opencode activity | OpenCube signal |
86
- | --- | --- |
87
- | Session becomes busy | One cube face lights up with a stable session color. |
88
- | Session becomes idle | The face glow is released once the cube slows down. |
89
- | A tool call starts | The session's face emits fast, bright sparks in that face's current outward direction. |
90
- | A tool call finishes | Spark emission stops; existing sparks fade out naturally. |
91
- | Right mouse hold on the cube | Friction increases and braking particles appear. |
92
- | Tray menu → Show Inbox | Opens a two-column event/debug panel. |
93
-
94
- Multiple busy sessions can light multiple faces. If more than six sessions are busy, OpenCube shows the latest six.
95
-
96
- ## How it works
97
-
98
- OpenCube has two parts in one npm package:
99
-
100
- 1. `src/plugin-server.cjs` — the opencode plugin entrypoint.
101
- 2. `src/main.js` — the Electron desktop pet.
102
-
103
- The plugin registers slash commands, listens for opencode `session.status` events and `tool.execute.*` hooks, and sends events to the desktop pet over a local HTTP API:
104
-
105
39
  ```text
106
- opencode plugin -> http://127.0.0.1:47832 -> Electron OpenCube
40
+ /pet start or show OpenCube
41
+ /pet_stop quit OpenCube
42
+ /pet-size show size usage
43
+ /pet-size 0.7 set cube size, range 0.3–3
44
+ /pet_update check npm for updates
45
+ /pet_say_hello flash a free face
46
+ /pet_fancy_say_hello run a small light show
107
47
  ```
108
48
 
109
- The Electron process owns the window, tray menu, Three.js renderer, cube rotation, face glow state, particle effects, and inbox/debug endpoints.
110
-
111
- ## Local API
112
-
113
- OpenCube exposes a local-only HTTP API while it is running:
114
-
115
- | Endpoint | Purpose |
116
- | --- | --- |
117
- | `GET /health` | Check whether OpenCube is running. |
118
- | `GET /debug-render` | Inspect renderer state, busy faces, active tools, and particle counters. |
119
- | `POST /event` | Receive opencode lifecycle/tool/hello events. |
120
- | `POST /interaction` | Receive local renderer mouse/keyboard diagnostics. |
121
- | `POST /show` | Show the cube window. |
122
- | `POST /quit` | Quit OpenCube. |
123
-
124
- The API binds to `127.0.0.1` only.
125
-
126
- ## Requirements
127
-
128
- - opencode
129
- - macOS, Windows, or Linux capable of running Electron
130
- - network access to install npm dependencies during first plugin install
131
-
132
- Users do not need to run `npm install` manually when installing via `opencode plugin`. opencode installs npm plugins and dependencies automatically.
133
-
134
49
  ## Notes
135
50
 
136
- - The first install may take a while because Electron is downloaded as a runtime dependency.
137
- - If Electron's platform binary is missing after installation, OpenCube attempts a first-run self-repair download.
138
- - If commands do not appear after installation, restart opencode.
139
- - OpenCube uses a local-only HTTP server on `127.0.0.1:47832`.
140
- - If that port is already in use, set `OPENCODE_PET_PORT` before starting opencode.
141
- - In opencode desktop, OpenCube currently uses a command-abort sentinel to keep local slash commands out of the model flow; depending on opencode version, the desktop UI may show an error toast even though the command was handled.
51
+ - OpenCube runs a local Electron process.
52
+ - Local API: `http://127.0.0.1:47832`.
53
+ - If commands do not appear, restart opencode.
54
+ - First install can take a bit because Electron is downloaded.
142
55
 
143
- ## Local development
144
-
145
- From this repository:
56
+ ## Development
146
57
 
147
58
  ```sh
148
59
  npm install
149
- npm run start
60
+ npm start
61
+ npm pack --dry-run
150
62
  ```
151
63
 
152
- For local opencode plugin testing, point your opencode config at the package directory:
64
+ For local plugin testing:
153
65
 
154
66
  ```json
155
67
  {
@@ -157,10 +69,4 @@ For local opencode plugin testing, point your opencode config at the package dir
157
69
  }
158
70
  ```
159
71
 
160
- After changing plugin files or opencode config, restart opencode.
161
-
162
- To inspect the package contents before publishing:
163
-
164
- ```sh
165
- npm pack --dry-run
166
- ```
72
+ Restart opencode after changing plugin code or config.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencube",
3
- "version": "0.3.3",
3
+ "version": "0.4.1",
4
4
  "description": "A tiny Three.js desktop pet for opencode session activity.",
5
5
  "main": "src/main.js",
6
6
  "exports": {
package/src/main.js CHANGED
@@ -17,6 +17,8 @@ const MAX_EVENTS = 100
17
17
  const MAX_INTERACTION_EVENTS = 80
18
18
  const IDLE_TTL_MS = 5 * 60 * 1000
19
19
  const PET_WINDOW_SIZE = 120
20
+ const MIN_PET_SIZE_SCALE = 0.3
21
+ const MAX_PET_SIZE_SCALE = 3
20
22
 
21
23
  let petWindow = null
22
24
  let panelWindow = null
@@ -31,6 +33,7 @@ let pendingPermissionsByRequest = new Map()
31
33
  let pendingQuestionsByRequest = new Map()
32
34
  let cleanupTimer = null
33
35
  let dragState = null
36
+ let petSizeScale = 1
34
37
 
35
38
  function normalizeDragPoint(point) {
36
39
  const screenX = Number(point?.screenX)
@@ -104,12 +107,22 @@ function ensureDataDir() {
104
107
  fs.mkdirSync(DATA_DIR, { recursive: true })
105
108
  }
106
109
 
110
+ function readState() {
111
+ try {
112
+ return JSON.parse(fs.readFileSync(STATE_FILE, "utf8"))
113
+ } catch {
114
+ return {}
115
+ }
116
+ }
117
+
107
118
  function writeState(extra = {}) {
108
119
  ensureDataDir()
120
+ const previous = readState()
109
121
  fs.writeFileSync(
110
122
  STATE_FILE,
111
123
  JSON.stringify(
112
124
  {
125
+ ...previous,
113
126
  pid: process.pid,
114
127
  startedAt: Date.now(),
115
128
  iconPath: ICON_PATH,
@@ -121,6 +134,34 @@ function writeState(extra = {}) {
121
134
  )
122
135
  }
123
136
 
137
+ function normalizePetSizeScale(value) {
138
+ const scale = Number(value)
139
+ if (!Number.isFinite(scale)) return undefined
140
+ return Math.max(MIN_PET_SIZE_SCALE, Math.min(MAX_PET_SIZE_SCALE, scale))
141
+ }
142
+
143
+ function getPetWindowSize() {
144
+ return Math.round(PET_WINDOW_SIZE * petSizeScale)
145
+ }
146
+
147
+ function loadPetSizeScale() {
148
+ petSizeScale = normalizePetSizeScale(readState().sizeScale) ?? 1
149
+ return petSizeScale
150
+ }
151
+
152
+ function applyPetSizeScale(scale) {
153
+ const nextScale = normalizePetSizeScale(scale)
154
+ if (nextScale === undefined) throw new Error(`sizeScale must be a number between ${MIN_PET_SIZE_SCALE} and ${MAX_PET_SIZE_SCALE}`)
155
+ petSizeScale = nextScale
156
+ const size = getPetWindowSize()
157
+ writeState({ sizeScale: petSizeScale, size: { width: size, height: size } })
158
+ if (petWindow && !petWindow.isDestroyed()) {
159
+ petWindow.setSize(size, size, false)
160
+ petWindow.loadFile(writePetHtmlFile())
161
+ }
162
+ return { scale: petSizeScale, width: size, height: size }
163
+ }
164
+
124
165
  function shouldQuit(argv = process.argv) {
125
166
  return argv.includes("--quit") || argv.includes("--stop") || argv.includes("stop") || argv.includes("quit")
126
167
  }
@@ -548,7 +589,7 @@ function petHtml() {
548
589
  </style>
549
590
  </head>
550
591
  <body>
551
- <div class="stage" title="opencode pet:拖拽移动,右键打开菜单">
592
+ <div class="stage">
552
593
  <div class="pet-art" aria-label="pixel opencode pet">${petBodySvg}</div>
553
594
  <div id="ball-layer"></div>
554
595
  </div>
@@ -732,7 +773,7 @@ function petHtml3D() {
732
773
  const initialStateJson = JSON.stringify(getPetState()).replaceAll("<", "\\u003c")
733
774
  const iconUrl = createIconDataUrl()
734
775
  const threeCjsPath = JSON.stringify(path.join(path.dirname(require.resolve("three")), "three.cjs"))
735
- const renderSize = PET_WINDOW_SIZE
776
+ const renderSize = getPetWindowSize()
736
777
  return `<!doctype html>
737
778
  <html>
738
779
  <head>
@@ -770,7 +811,7 @@ function petHtml3D() {
770
811
  </style>
771
812
  </head>
772
813
  <body>
773
- <div class="stage" title="opencode pet:拖拽移动,右键打开菜单">
814
+ <div class="stage">
774
815
  <canvas id="scene" aria-label="3D opencode pet"></canvas>
775
816
  <div id="hit-layer" aria-label="OpenCube interaction layer"></div>
776
817
  </div>
@@ -1737,9 +1778,10 @@ function restorePosition(win) {
1737
1778
 
1738
1779
  function createPetWindow() {
1739
1780
  if (petWindow && !petWindow.isDestroyed()) return petWindow
1781
+ const size = getPetWindowSize()
1740
1782
  petWindow = new BrowserWindow({
1741
- width: PET_WINDOW_SIZE,
1742
- height: PET_WINDOW_SIZE,
1783
+ width: size,
1784
+ height: size,
1743
1785
  show: false,
1744
1786
  frame: false,
1745
1787
  resizable: false,
@@ -1895,6 +1937,7 @@ function startServer() {
1895
1937
  port: PORT,
1896
1938
  events: events.length,
1897
1939
  pet: "opencube",
1940
+ size: { scale: petSizeScale, width: getPetWindowSize(), height: getPetWindowSize() },
1898
1941
  sessions: getPetState().sessions.map(({ sessionID, state, busyIndex, idleIndex, color }) => ({
1899
1942
  sessionID,
1900
1943
  state,
@@ -1944,6 +1987,12 @@ function startServer() {
1944
1987
  return json(res, 200, { ok: true })
1945
1988
  }
1946
1989
 
1990
+ if (req.method === "POST" && url.pathname === "/size") {
1991
+ const body = await readRequestJson(req)
1992
+ const size = applyPetSizeScale(body?.scale)
1993
+ return json(res, 200, { ok: true, size })
1994
+ }
1995
+
1947
1996
  if (req.method === "POST" && url.pathname === "/toggle-panel") {
1948
1997
  togglePanel()
1949
1998
  return json(res, 200, { ok: true, visible: panelWindow?.isVisible() === true })
@@ -1968,6 +2017,7 @@ function startServer() {
1968
2017
 
1969
2018
  function start() {
1970
2019
  app.dock?.hide()
2020
+ loadPetSizeScale()
1971
2021
  writeState({ visible: false, mode: "floating" })
1972
2022
  createTray()
1973
2023
  startServer()
@@ -1,9 +1,11 @@
1
- const { quitPet, sendEvent, showPet } = require("./plugin-shared.cjs")
1
+ const { quitPet, requestPet, sendEvent, showPet } = require("./plugin-shared.cjs")
2
2
  const pkg = require("../package.json")
3
3
 
4
4
  const COMMAND_HANDLED_SENTINEL = "__OPENCODE_PET_COMMAND_HANDLED__"
5
5
  const CUB_ICON = "◈"
6
6
  const UPDATE_CHECK_TIMEOUT_MS = 5000
7
+ const MIN_PET_SIZE_SCALE = 0.3
8
+ const MAX_PET_SIZE_SCALE = 3
7
9
 
8
10
  function handled() {
9
11
  throw new Error(COMMAND_HANDLED_SENTINEL)
@@ -33,6 +35,31 @@ function isUpdateCheck(input) {
33
35
  return input.command === "pet_update" || input.command === "pet_upgrade"
34
36
  }
35
37
 
38
+ function isPetSize(input) {
39
+ return input.command === "pet-size" || input.command === "pet_size"
40
+ }
41
+
42
+ function petSizeUsage() {
43
+ return cubNotice(
44
+ [
45
+ "Set OpenCube size with: /pet-size 0.7",
46
+ `Allowed scale: ${MIN_PET_SIZE_SCALE} to ${MAX_PET_SIZE_SCALE}.`,
47
+ "Run /pet first if OpenCube is sleeping.",
48
+ ].join("\n"),
49
+ "↔",
50
+ )
51
+ }
52
+
53
+ function parsePetSizeScale(args) {
54
+ const text = textOfArguments(args)
55
+ if (!text) return undefined
56
+ const first = text.split(/\s+/)[0]
57
+ const scale = Number(first)
58
+ if (!Number.isFinite(scale)) return undefined
59
+ if (scale < MIN_PET_SIZE_SCALE || scale > MAX_PET_SIZE_SCALE) return undefined
60
+ return scale
61
+ }
62
+
36
63
  function compareVersions(a, b) {
37
64
  const left = String(a || "").split(".").map((part) => Number.parseInt(part, 10) || 0)
38
65
  const right = String(b || "").split(".").map((part) => Number.parseInt(part, 10) || 0)
@@ -161,10 +188,18 @@ module.exports = {
161
188
  template: "/pet_upgrade",
162
189
  description: "Alias for /pet_update.",
163
190
  }
191
+ cfg.command["pet-size"] = {
192
+ template: "/pet-size",
193
+ description: "Show or set OpenCube size, for example /pet-size 0.7.",
194
+ }
195
+ cfg.command.pet_size = {
196
+ template: "/pet_size",
197
+ description: "Alias for /pet-size.",
198
+ }
164
199
  },
165
200
 
166
201
  "command.execute.before": async (input, output) => {
167
- if (!["pet", "pet_stop", "pet_say_hello", "pet_fancy_say_hello", "pet_update", "pet_upgrade"].includes(input.command)) return
202
+ if (!["pet", "pet_stop", "pet_say_hello", "pet_fancy_say_hello", "pet_update", "pet_upgrade", "pet-size", "pet_size"].includes(input.command)) return
168
203
 
169
204
  // There is no official cancel primitive in command.execute.before yet.
170
205
  // Throwing this sentinel aborts the command flow before opencode sends
@@ -238,6 +273,22 @@ module.exports = {
238
273
  }
239
274
 
240
275
  await injectNotice(client, input.sessionID, formatUpdateNotice(latest))
276
+ } else if (isPetSize(input)) {
277
+ const scale = parsePetSizeScale(input.arguments)
278
+ if (scale === undefined) {
279
+ await injectNotice(client, input.sessionID, petSizeUsage())
280
+ } else {
281
+ const result = await requestPet("/size", { method: "POST", body: { scale }, timeoutMs: 1000 })
282
+ if (result?.ok && result.size) {
283
+ await injectNotice(
284
+ client,
285
+ input.sessionID,
286
+ cubNotice(`OpenCube size set to ${result.size.scale} (${result.size.width}×${result.size.height}).`, "↔"),
287
+ )
288
+ } else {
289
+ await injectNotice(client, input.sessionID, cubNotice("OpenCube is sleeping... zzz Run /pet first, then try /pet-size 0.7.", "☾"))
290
+ }
291
+ }
241
292
  } else {
242
293
  await showPet({
243
294
  onProgress: (message) => injectNotice(client, input.sessionID, message),