opencube 0.1.0 → 0.1.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 +12 -14
- package/package.json +1 -1
- package/src/main.js +2 -2
- package/src/plugin-server.cjs +11 -11
- package/src/plugin-shared.cjs +19 -19
- package/src/plugin-tui.cjs +1 -1
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="assets/opencode-icon.png" width="96" height="96" alt="
|
|
2
|
+
<img src="assets/opencode-icon.png" width="96" height="96" alt="OpenCube icon" />
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
#
|
|
5
|
+
# OpenCube
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
OpenCube is a tiny desktop pet for [opencode](https://opencode.ai/).
|
|
8
8
|
|
|
9
9
|
It watches opencode session activity and renders a small Three.js cube on your desktop:
|
|
10
10
|
|
|
@@ -13,16 +13,14 @@ It watches opencode session activity and renders a small Three.js cube on your d
|
|
|
13
13
|
- `/pet_say_hello` flashes one free face
|
|
14
14
|
- `/pet_fancy_say_hello` plays a randomized light show on free faces
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
OpenCube is packaged as an opencode plugin plus an Electron desktop process.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
20
|
-
> OpenCub is not published yet. These are the intended install commands once published.
|
|
21
|
-
|
|
22
20
|
Install globally through opencode:
|
|
23
21
|
|
|
24
22
|
```sh
|
|
25
|
-
opencode plugin
|
|
23
|
+
opencode plugin opencube --global
|
|
26
24
|
```
|
|
27
25
|
|
|
28
26
|
Then restart opencode and run:
|
|
@@ -35,7 +33,7 @@ You can also add it manually to `~/.config/opencode/opencode.json`:
|
|
|
35
33
|
|
|
36
34
|
```json
|
|
37
35
|
{
|
|
38
|
-
"plugin": ["
|
|
36
|
+
"plugin": ["opencube"]
|
|
39
37
|
}
|
|
40
38
|
```
|
|
41
39
|
|
|
@@ -43,8 +41,8 @@ You can also add it manually to `~/.config/opencode/opencode.json`:
|
|
|
43
41
|
|
|
44
42
|
| Command | Description |
|
|
45
43
|
| --- | --- |
|
|
46
|
-
| `/pet` | Show or start
|
|
47
|
-
| `/pet_stop` | Quit
|
|
44
|
+
| `/pet` | Show or start OpenCube. |
|
|
45
|
+
| `/pet_stop` | Quit OpenCube. |
|
|
48
46
|
| `/pet_say_hello` | Flash one currently free face three times with a random color. |
|
|
49
47
|
| `/pet_fancy_say_hello` | Run a denser randomized light show across currently free faces. |
|
|
50
48
|
|
|
@@ -52,7 +50,7 @@ These commands are handled by the plugin and do not get sent to the model.
|
|
|
52
50
|
|
|
53
51
|
## How it works
|
|
54
52
|
|
|
55
|
-
|
|
53
|
+
OpenCube has two parts in one npm package:
|
|
56
54
|
|
|
57
55
|
1. `src/plugin-server.cjs` — the opencode plugin entrypoint.
|
|
58
56
|
2. `src/main.js` — the Electron desktop pet.
|
|
@@ -60,7 +58,7 @@ OpenCub has two parts in one npm package:
|
|
|
60
58
|
The plugin registers slash commands, listens for opencode `session.status` events, and sends events to the desktop pet over a local HTTP API:
|
|
61
59
|
|
|
62
60
|
```text
|
|
63
|
-
opencode plugin -> http://127.0.0.1:47832 -> Electron
|
|
61
|
+
opencode plugin -> http://127.0.0.1:47832 -> Electron OpenCube
|
|
64
62
|
```
|
|
65
63
|
|
|
66
64
|
The Electron process owns the window, Three.js renderer, cube rotation, face glow state, and inbox/debug endpoints.
|
|
@@ -77,7 +75,7 @@ Users do not need to run `npm install` manually when installing via `opencode pl
|
|
|
77
75
|
|
|
78
76
|
- The first install may take a while because Electron is downloaded as a runtime dependency.
|
|
79
77
|
- If commands do not appear after installation, restart opencode.
|
|
80
|
-
-
|
|
78
|
+
- OpenCube uses a local-only HTTP server on `127.0.0.1:47832`.
|
|
81
79
|
- If that port is already in use, set `OPENCODE_PET_PORT` before starting opencode.
|
|
82
80
|
|
|
83
81
|
## Local development
|
|
@@ -93,7 +91,7 @@ For local opencode plugin testing, point your opencode config at the package dir
|
|
|
93
91
|
|
|
94
92
|
```json
|
|
95
93
|
{
|
|
96
|
-
"plugin": ["/path/to/
|
|
94
|
+
"plugin": ["/path/to/opencube"]
|
|
97
95
|
}
|
|
98
96
|
```
|
|
99
97
|
|
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -9,7 +9,7 @@ const { DEFAULT_SESSION_COLORS, pixelPetSvg } = require("./pixel-pet-reference.c
|
|
|
9
9
|
const APP_NAME = "opencode pet"
|
|
10
10
|
const HOST = "127.0.0.1"
|
|
11
11
|
const PORT = Number(process.env.OPENCODE_PET_PORT || 47832)
|
|
12
|
-
const DATA_DIR = path.join(os.homedir(), ".local", "share", "
|
|
12
|
+
const DATA_DIR = path.join(os.homedir(), ".local", "share", "opencube")
|
|
13
13
|
const STATE_FILE = path.join(DATA_DIR, "state.json")
|
|
14
14
|
const PET_HTML_FILE = path.join(DATA_DIR, "pet.html")
|
|
15
15
|
const ICON_PATH = process.env.OPENCODE_PET_ICON || path.join(__dirname, "..", "assets", "opencode-icon.png")
|
|
@@ -1169,7 +1169,7 @@ function startServer() {
|
|
|
1169
1169
|
pid: process.pid,
|
|
1170
1170
|
port: PORT,
|
|
1171
1171
|
events: events.length,
|
|
1172
|
-
pet: "
|
|
1172
|
+
pet: "opencube",
|
|
1173
1173
|
sessions: getPetState().sessions.map(({ sessionID, state, busyIndex, idleIndex, color }) => ({
|
|
1174
1174
|
sessionID,
|
|
1175
1175
|
state,
|
package/src/plugin-server.cjs
CHANGED
|
@@ -48,7 +48,7 @@ function cubNotice(text, icon = CUB_ICON) {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
module.exports = {
|
|
51
|
-
id: "
|
|
51
|
+
id: "opencube",
|
|
52
52
|
server: async ({ client }) => {
|
|
53
53
|
const sessionStatus = new Map()
|
|
54
54
|
|
|
@@ -68,11 +68,11 @@ module.exports = {
|
|
|
68
68
|
}
|
|
69
69
|
cfg.command.pet_say_hello = {
|
|
70
70
|
template: "/pet_say_hello",
|
|
71
|
-
description: "Send a hello test event to
|
|
71
|
+
description: "Send a hello test event to OpenCube.",
|
|
72
72
|
}
|
|
73
73
|
cfg.command.pet_fancy_say_hello = {
|
|
74
74
|
template: "/pet_fancy_say_hello",
|
|
75
|
-
description: "Trigger a randomized light show on
|
|
75
|
+
description: "Trigger a randomized light show on OpenCube's free faces.",
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
|
|
@@ -87,7 +87,7 @@ module.exports = {
|
|
|
87
87
|
|
|
88
88
|
if (shouldQuit(input)) {
|
|
89
89
|
await quitPet()
|
|
90
|
-
await injectNotice(client, input.sessionID, cubNotice("
|
|
90
|
+
await injectNotice(client, input.sessionID, cubNotice("OpenCube is going to sleep 🐾", "◌"))
|
|
91
91
|
} else if (isSayHello(input)) {
|
|
92
92
|
const result = await sendEvent({
|
|
93
93
|
type: "hello",
|
|
@@ -95,12 +95,12 @@ module.exports = {
|
|
|
95
95
|
command: input.command,
|
|
96
96
|
arguments: input.arguments,
|
|
97
97
|
sessionID: input.sessionID,
|
|
98
|
-
source: "
|
|
98
|
+
source: "opencube-plugin",
|
|
99
99
|
})
|
|
100
100
|
await injectNotice(
|
|
101
101
|
client,
|
|
102
102
|
input.sessionID,
|
|
103
|
-
result ? cubNotice("
|
|
103
|
+
result ? cubNotice("OpenCube got your hello 🐾", "✦") : cubNotice("OpenCube is sleeping... zzz Use /pet to wake it.", "☾"),
|
|
104
104
|
)
|
|
105
105
|
} else if (isFancySayHello(input)) {
|
|
106
106
|
const result = await sendEvent({
|
|
@@ -109,14 +109,14 @@ module.exports = {
|
|
|
109
109
|
command: input.command,
|
|
110
110
|
arguments: input.arguments,
|
|
111
111
|
sessionID: input.sessionID,
|
|
112
|
-
source: "
|
|
112
|
+
source: "opencube-plugin",
|
|
113
113
|
})
|
|
114
114
|
await injectNotice(
|
|
115
115
|
client,
|
|
116
116
|
input.sessionID,
|
|
117
117
|
result
|
|
118
|
-
? cubNotice("
|
|
119
|
-
: cubNotice("
|
|
118
|
+
? cubNotice("OpenCube is putting on a light show ✨", "✺")
|
|
119
|
+
: cubNotice("OpenCube is sleeping... zzz Start it with /pet before the light show.", "☾"),
|
|
120
120
|
)
|
|
121
121
|
} else {
|
|
122
122
|
await showPet({
|
|
@@ -146,7 +146,7 @@ module.exports = {
|
|
|
146
146
|
sessionID,
|
|
147
147
|
status,
|
|
148
148
|
previousStatus: previous,
|
|
149
|
-
source: "
|
|
149
|
+
source: "opencube-plugin",
|
|
150
150
|
})
|
|
151
151
|
return
|
|
152
152
|
}
|
|
@@ -161,7 +161,7 @@ module.exports = {
|
|
|
161
161
|
sessionID,
|
|
162
162
|
status,
|
|
163
163
|
previousStatus: previous,
|
|
164
|
-
source: "
|
|
164
|
+
source: "opencube-plugin",
|
|
165
165
|
})
|
|
166
166
|
},
|
|
167
167
|
}
|
package/src/plugin-shared.cjs
CHANGED
|
@@ -30,7 +30,7 @@ async function emitProgress(onProgress, message) {
|
|
|
30
30
|
try {
|
|
31
31
|
await onProgress(message)
|
|
32
32
|
} catch {
|
|
33
|
-
// Progress is best-effort; never block
|
|
33
|
+
// Progress is best-effort; never block OpenCube startup on UI notices.
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -76,11 +76,11 @@ async function installElectronBinary(electronDir, options = {}) {
|
|
|
76
76
|
const executablePath = path.join(distPath, platformPath)
|
|
77
77
|
|
|
78
78
|
if (fs.existsSync(executablePath)) {
|
|
79
|
-
await emitProgress(options.onProgress, "
|
|
79
|
+
await emitProgress(options.onProgress, "OpenCube: Electron binary is ready ✅")
|
|
80
80
|
return executablePath
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
await emitProgress(options.onProgress, `
|
|
83
|
+
await emitProgress(options.onProgress, `OpenCube: downloading Electron ${version} for ${platform}/${arch}...`)
|
|
84
84
|
const zipPath = await downloadArtifact({
|
|
85
85
|
version,
|
|
86
86
|
artifactName: "electron",
|
|
@@ -89,19 +89,19 @@ async function installElectronBinary(electronDir, options = {}) {
|
|
|
89
89
|
platform,
|
|
90
90
|
arch,
|
|
91
91
|
})
|
|
92
|
-
await emitProgress(options.onProgress, "
|
|
92
|
+
await emitProgress(options.onProgress, "OpenCube: extracting Electron binary...")
|
|
93
93
|
await extractElectronZip(zipPath, distPath)
|
|
94
94
|
await fs.promises.writeFile(path.join(electronDir, "path.txt"), platformPath)
|
|
95
|
-
await emitProgress(options.onProgress, "
|
|
95
|
+
await emitProgress(options.onProgress, "OpenCube: Electron binary installed ✅")
|
|
96
96
|
return executablePath
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
async function resolveElectronPath(options = {}) {
|
|
100
|
-
await emitProgress(options.onProgress, "
|
|
100
|
+
await emitProgress(options.onProgress, "OpenCube: checking Electron runtime...")
|
|
101
101
|
try {
|
|
102
102
|
const electronPath = require("electron")
|
|
103
103
|
if (typeof electronPath === "string") {
|
|
104
|
-
await emitProgress(options.onProgress, "
|
|
104
|
+
await emitProgress(options.onProgress, "OpenCube: Electron runtime is ready ✅")
|
|
105
105
|
return electronPath
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -109,12 +109,12 @@ async function resolveElectronPath(options = {}) {
|
|
|
109
109
|
// environment require("electron") can resolve to Electron's built-in API
|
|
110
110
|
// object instead of the npm package's executable path string. Fall through
|
|
111
111
|
// to the npm package directory and resolve/repair the packaged binary.
|
|
112
|
-
await emitProgress(options.onProgress, "
|
|
112
|
+
await emitProgress(options.onProgress, "OpenCube: locating packaged Electron binary...")
|
|
113
113
|
const electronPackage = require.resolve("electron/package.json")
|
|
114
114
|
const electronDir = path.dirname(electronPackage)
|
|
115
115
|
return await installElectronBinary(electronDir, options)
|
|
116
116
|
} catch (error) {
|
|
117
|
-
await emitProgress(options.onProgress, "
|
|
117
|
+
await emitProgress(options.onProgress, "OpenCube: Electron runtime is incomplete; repairing...")
|
|
118
118
|
const electronPackage = require.resolve("electron/package.json")
|
|
119
119
|
const electronDir = path.dirname(electronPackage)
|
|
120
120
|
return await installElectronBinary(electronDir, options)
|
|
@@ -123,7 +123,7 @@ async function resolveElectronPath(options = {}) {
|
|
|
123
123
|
|
|
124
124
|
async function launchPet(args = [], options = {}) {
|
|
125
125
|
const electronPath = await resolveElectronPath(options)
|
|
126
|
-
await emitProgress(options.onProgress, "
|
|
126
|
+
await emitProgress(options.onProgress, "OpenCube: launching desktop pet...")
|
|
127
127
|
const child = spawn(electronPath, [PET_APP_DIR, ...args], {
|
|
128
128
|
cwd: PET_APP_DIR,
|
|
129
129
|
detached: true,
|
|
@@ -134,7 +134,7 @@ async function launchPet(args = [], options = {}) {
|
|
|
134
134
|
},
|
|
135
135
|
})
|
|
136
136
|
child.unref()
|
|
137
|
-
await emitProgress(options.onProgress, "
|
|
137
|
+
await emitProgress(options.onProgress, "OpenCube: launch request sent 🐾")
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
async function requestPet(pathname, options = {}) {
|
|
@@ -165,28 +165,28 @@ async function healthPet() {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
async function waitForPet(timeoutMs = 3500, options = {}) {
|
|
168
|
-
await emitProgress(options.onProgress, "
|
|
168
|
+
await emitProgress(options.onProgress, "OpenCube: waiting for local server...")
|
|
169
169
|
const startedAt = Date.now()
|
|
170
170
|
while (Date.now() - startedAt < timeoutMs) {
|
|
171
171
|
const health = await healthPet()
|
|
172
172
|
if (health) {
|
|
173
|
-
await emitProgress(options.onProgress, "
|
|
173
|
+
await emitProgress(options.onProgress, "OpenCube: local server is ready ✅")
|
|
174
174
|
return health
|
|
175
175
|
}
|
|
176
176
|
await new Promise((resolve) => setTimeout(resolve, 150))
|
|
177
177
|
}
|
|
178
|
-
await emitProgress(options.onProgress, "
|
|
178
|
+
await emitProgress(options.onProgress, "OpenCube: local server did not answer yet")
|
|
179
179
|
return undefined
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
async function ensurePet(options = {}) {
|
|
183
|
-
await emitProgress(options.onProgress, "
|
|
183
|
+
await emitProgress(options.onProgress, "OpenCube: checking whether it is already running...")
|
|
184
184
|
const existing = await healthPet()
|
|
185
185
|
if (existing) {
|
|
186
|
-
await emitProgress(options.onProgress, "
|
|
186
|
+
await emitProgress(options.onProgress, "OpenCube: already running; showing window...")
|
|
187
187
|
return existing
|
|
188
188
|
}
|
|
189
|
-
await emitProgress(options.onProgress, "
|
|
189
|
+
await emitProgress(options.onProgress, "OpenCube: not running; starting now...")
|
|
190
190
|
await launchPet(["--show"], options)
|
|
191
191
|
return await waitForPet(3500, options)
|
|
192
192
|
}
|
|
@@ -194,7 +194,7 @@ async function ensurePet(options = {}) {
|
|
|
194
194
|
async function showPet(options = {}) {
|
|
195
195
|
const health = await ensurePet(options)
|
|
196
196
|
await requestPet("/show", { method: "POST", timeoutMs: 800 })
|
|
197
|
-
await emitProgress(options.onProgress, health ? "
|
|
197
|
+
await emitProgress(options.onProgress, health ? "OpenCube: shown ✨" : "OpenCube: start requested, still warming up...")
|
|
198
198
|
return health
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -208,7 +208,7 @@ async function quitPet() {
|
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
async function sendEvent(event) {
|
|
211
|
-
// Only /pet is allowed to start
|
|
211
|
+
// Only /pet is allowed to start OpenCube. Session lifecycle events and hello
|
|
212
212
|
// commands should talk to the desktop pet only if it is already running.
|
|
213
213
|
const health = await healthPet()
|
|
214
214
|
if (!health) return undefined
|
package/src/plugin-tui.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
module.exports = {
|
|
2
|
-
id: "
|
|
2
|
+
id: "opencube",
|
|
3
3
|
// Slash commands are registered from the server plugin via cfg.command, using
|
|
4
4
|
// the same command.execute.before abort trick as @slkiser/opencode-quota.
|
|
5
5
|
// Keep a no-op TUI entry so opencode can load ./tui without duplicate /pet rows.
|