opencube 0.2.1 → 0.3.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 +27 -1
- package/package.json +1 -1
- package/src/main.js +43 -8
- package/src/plugin-server.cjs +149 -1
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ OpenCube is packaged as an opencode plugin plus an Electron desktop process.
|
|
|
21
21
|
|
|
22
22
|
## Install
|
|
23
23
|
|
|
24
|
-
Install globally through opencode:
|
|
24
|
+
Install the latest published version globally through opencode:
|
|
25
25
|
|
|
26
26
|
```sh
|
|
27
27
|
opencode plugin opencube --global
|
|
@@ -41,6 +41,30 @@ You can also add it manually to `~/.config/opencode/opencode.json`:
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
## Update
|
|
45
|
+
|
|
46
|
+
If you already installed OpenCube and want to upgrade, reinstall the target version with `--force`:
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
opencode plugin opencube@0.3.1 --global --force
|
|
50
|
+
```
|
|
51
|
+
|
|
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:
|
|
53
|
+
|
|
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:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
/pet_update
|
|
64
|
+
```
|
|
65
|
+
|
|
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
|
+
|
|
44
68
|
## Commands
|
|
45
69
|
|
|
46
70
|
| Command | Description |
|
|
@@ -49,6 +73,8 @@ You can also add it manually to `~/.config/opencode/opencode.json`:
|
|
|
49
73
|
| `/pet_stop` | Quit OpenCube. |
|
|
50
74
|
| `/pet_say_hello` | Flash one currently free face three times with a random color. |
|
|
51
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`. |
|
|
52
78
|
|
|
53
79
|
These commands are handled by the plugin and do not get sent to the model.
|
|
54
80
|
|
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -27,6 +27,7 @@ let petSignals = []
|
|
|
27
27
|
let sessionMap = new Map()
|
|
28
28
|
let activeToolsBySession = new Map()
|
|
29
29
|
let pendingPermissionsByRequest = new Map()
|
|
30
|
+
let pendingQuestionsByRequest = new Map()
|
|
30
31
|
let cleanupTimer = null
|
|
31
32
|
let dragState = null
|
|
32
33
|
|
|
@@ -133,6 +134,7 @@ function recordEvent(event) {
|
|
|
133
134
|
applySessionEvent(item)
|
|
134
135
|
applyToolEvent(item)
|
|
135
136
|
applyPermissionEvent(item)
|
|
137
|
+
applyQuestionEvent(item)
|
|
136
138
|
if (item.type === "hello" || item.type === "fancy_hello") {
|
|
137
139
|
petSignals.push({
|
|
138
140
|
id: item.id,
|
|
@@ -180,6 +182,33 @@ function clearPendingPermissionsForSession(sessionID) {
|
|
|
180
182
|
}
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
function applyQuestionEvent(event) {
|
|
186
|
+
if (!event || typeof event.sessionID !== "string") return
|
|
187
|
+
|
|
188
|
+
if (event.type === "question.ask") {
|
|
189
|
+
const requestID = typeof event.requestID === "string" ? event.requestID : event.id
|
|
190
|
+
if (!requestID) return
|
|
191
|
+
pendingQuestionsByRequest.set(requestID, {
|
|
192
|
+
requestID,
|
|
193
|
+
sessionID: event.sessionID,
|
|
194
|
+
questions: event.questions,
|
|
195
|
+
tool: event.tool,
|
|
196
|
+
askedAt: event.receivedAt || Date.now(),
|
|
197
|
+
})
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (event.type === "question.reply" || event.type === "question.reject") {
|
|
202
|
+
if (typeof event.requestID === "string") pendingQuestionsByRequest.delete(event.requestID)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function clearPendingQuestionsForSession(sessionID) {
|
|
207
|
+
for (const [requestID, question] of pendingQuestionsByRequest) {
|
|
208
|
+
if (question?.sessionID === sessionID) pendingQuestionsByRequest.delete(requestID)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
183
212
|
function applyToolEvent(event) {
|
|
184
213
|
if (!event || typeof event.sessionID !== "string" || typeof event.callID !== "string") return
|
|
185
214
|
if (event.type !== "tool.start" && event.type !== "tool.finish") return
|
|
@@ -237,6 +266,7 @@ function applySessionEvent(event) {
|
|
|
237
266
|
if (event.type === "session.idle") {
|
|
238
267
|
activeToolsBySession.delete(event.sessionID)
|
|
239
268
|
clearPendingPermissionsForSession(event.sessionID)
|
|
269
|
+
clearPendingQuestionsForSession(event.sessionID)
|
|
240
270
|
sessionMap.set(event.sessionID, {
|
|
241
271
|
sessionID: event.sessionID,
|
|
242
272
|
state: "idle",
|
|
@@ -259,6 +289,7 @@ function pruneIdleSessions(refresh = true) {
|
|
|
259
289
|
sessionMap.delete(sessionID)
|
|
260
290
|
activeToolsBySession.delete(sessionID)
|
|
261
291
|
clearPendingPermissionsForSession(sessionID)
|
|
292
|
+
clearPendingQuestionsForSession(sessionID)
|
|
262
293
|
changed = true
|
|
263
294
|
}
|
|
264
295
|
}
|
|
@@ -294,6 +325,7 @@ function getPetState() {
|
|
|
294
325
|
color: DEFAULT_SESSION_COLORS[index % DEFAULT_SESSION_COLORS.length],
|
|
295
326
|
})),
|
|
296
327
|
permissions: Array.from(pendingPermissionsByRequest.values()),
|
|
328
|
+
questions: Array.from(pendingQuestionsByRequest.values()),
|
|
297
329
|
signals: petSignals,
|
|
298
330
|
}
|
|
299
331
|
}
|
|
@@ -759,7 +791,7 @@ function petHtml3D() {
|
|
|
759
791
|
const dragParticleTexture = createDragParticleTexture()
|
|
760
792
|
const faceGeometry = new THREE.PlaneGeometry(0.60, 0.60)
|
|
761
793
|
const glowGeometry = new THREE.PlaneGeometry(1.18, 1.18)
|
|
762
|
-
const permissionGlowGeometry = new THREE.PlaneGeometry(1.
|
|
794
|
+
const permissionGlowGeometry = new THREE.PlaneGeometry(1.62, 1.62)
|
|
763
795
|
const rad = THREE.MathUtils.degToRad
|
|
764
796
|
|
|
765
797
|
function createGlowTexture() {
|
|
@@ -1279,7 +1311,7 @@ function petHtml3D() {
|
|
|
1279
1311
|
permissionGlowGeometry,
|
|
1280
1312
|
new THREE.MeshBasicMaterial({
|
|
1281
1313
|
map: glowTexture,
|
|
1282
|
-
color:
|
|
1314
|
+
color: 0xffffff,
|
|
1283
1315
|
transparent: true,
|
|
1284
1316
|
opacity: 0,
|
|
1285
1317
|
blending: THREE.AdditiveBlending,
|
|
@@ -1287,7 +1319,7 @@ function petHtml3D() {
|
|
|
1287
1319
|
side: THREE.DoubleSide,
|
|
1288
1320
|
}),
|
|
1289
1321
|
)
|
|
1290
|
-
permissionGlow.position.set(...glowPosition.map((value) => value === 0 ? 0 : value * 1.
|
|
1322
|
+
permissionGlow.position.set(...glowPosition.map((value) => value === 0 ? 0 : value * 1.072))
|
|
1291
1323
|
permissionGlow.rotation.set(...rotation)
|
|
1292
1324
|
cubeGroup.add(permissionGlow)
|
|
1293
1325
|
permissionGlowMeshes.set(name, permissionGlow)
|
|
@@ -1494,13 +1526,15 @@ function petHtml3D() {
|
|
|
1494
1526
|
const sessionID = Array.from(pendingSessionIDs).find((id) => sessionFaceMap.get(id) === faceName)
|
|
1495
1527
|
if (!sessionID) {
|
|
1496
1528
|
permissionGlow.material.opacity = 0
|
|
1529
|
+
permissionGlow.scale.setScalar(1)
|
|
1497
1530
|
continue
|
|
1498
1531
|
}
|
|
1499
1532
|
|
|
1500
|
-
|
|
1501
|
-
permissionGlow.material.
|
|
1502
|
-
permissionGlow.
|
|
1503
|
-
|
|
1533
|
+
const color = sessionColorMap.get(sessionID) || randomSessionGlowColor()
|
|
1534
|
+
permissionGlow.material.color.setRGB(color.r / 255, color.g / 255, color.b / 255)
|
|
1535
|
+
permissionGlow.material.opacity = 0.14 + pulse * 0.60
|
|
1536
|
+
permissionGlow.scale.setScalar(1.06 + pulse * 0.42)
|
|
1537
|
+
active[faceName] = { sessionID, pulse, pending: true, color: color.name, rgb: [color.r, color.g, color.b] }
|
|
1504
1538
|
}
|
|
1505
1539
|
|
|
1506
1540
|
return active
|
|
@@ -1565,7 +1599,8 @@ function petHtml3D() {
|
|
|
1565
1599
|
const toolParticles = updateToolParticles(sessions, dt, now)
|
|
1566
1600
|
processSignals(snapshot.signals || [], now)
|
|
1567
1601
|
const helloFlashes = applyFlashFaces(now)
|
|
1568
|
-
const
|
|
1602
|
+
const pendingAttention = [...(snapshot.permissions || []), ...(snapshot.questions || [])]
|
|
1603
|
+
const permissionGlows = applyPermissionGlowFaces(pendingAttention, now)
|
|
1569
1604
|
renderCube()
|
|
1570
1605
|
latestDebug = {
|
|
1571
1606
|
now: Date.now(),
|
package/src/plugin-server.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const { quitPet, sendEvent, showPet } = require("./plugin-shared.cjs")
|
|
2
|
+
const pkg = require("../package.json")
|
|
2
3
|
|
|
3
4
|
const COMMAND_HANDLED_SENTINEL = "__OPENCODE_PET_COMMAND_HANDLED__"
|
|
4
5
|
const CUB_ICON = "◈"
|
|
6
|
+
const UPDATE_CHECK_TIMEOUT_MS = 5000
|
|
5
7
|
|
|
6
8
|
function handled() {
|
|
7
9
|
throw new Error(COMMAND_HANDLED_SENTINEL)
|
|
@@ -27,6 +29,72 @@ function isFancySayHello(input) {
|
|
|
27
29
|
return input.command === "pet_fancy_say_hello"
|
|
28
30
|
}
|
|
29
31
|
|
|
32
|
+
function isUpdateCheck(input) {
|
|
33
|
+
return input.command === "pet_update" || input.command === "pet_upgrade"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compareVersions(a, b) {
|
|
37
|
+
const left = String(a || "").split(".").map((part) => Number.parseInt(part, 10) || 0)
|
|
38
|
+
const right = String(b || "").split(".").map((part) => Number.parseInt(part, 10) || 0)
|
|
39
|
+
const length = Math.max(left.length, right.length)
|
|
40
|
+
for (let index = 0; index < length; index += 1) {
|
|
41
|
+
const diff = (left[index] || 0) - (right[index] || 0)
|
|
42
|
+
if (diff !== 0) return diff > 0 ? 1 : -1
|
|
43
|
+
}
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchLatestPackageVersion(name = pkg.name, timeoutMs = UPDATE_CHECK_TIMEOUT_MS) {
|
|
48
|
+
const controller = new AbortController()
|
|
49
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(name)}/latest`, {
|
|
53
|
+
headers: { accept: "application/json" },
|
|
54
|
+
signal: controller.signal,
|
|
55
|
+
})
|
|
56
|
+
if (!response.ok) throw new Error(`npm registry returned ${response.status}`)
|
|
57
|
+
const info = await response.json()
|
|
58
|
+
return {
|
|
59
|
+
name: info.name || name,
|
|
60
|
+
version: info.version,
|
|
61
|
+
tarball: info.dist?.tarball,
|
|
62
|
+
}
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (error?.name === "AbortError") throw new Error(`npm registry check timed out after ${timeoutMs}ms`)
|
|
65
|
+
throw error
|
|
66
|
+
} finally {
|
|
67
|
+
clearTimeout(timeout)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatUpdateNotice(latest) {
|
|
72
|
+
const current = pkg.version
|
|
73
|
+
if (!latest?.version) return cubNotice(`Could not read the latest ${pkg.name} version from npm.`, "△")
|
|
74
|
+
|
|
75
|
+
const comparison = compareVersions(latest.version, current)
|
|
76
|
+
if (comparison < 0) {
|
|
77
|
+
return cubNotice(
|
|
78
|
+
[
|
|
79
|
+
`Welcome to the tiny time machine ✨`,
|
|
80
|
+
`This OpenCube is a dev build (${current}), ahead of npm (${latest.version}).`,
|
|
81
|
+
`Keep this cube away from paradoxes and production users.`,
|
|
82
|
+
].join("\n"),
|
|
83
|
+
"✧",
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
if (comparison === 0) return cubNotice(`OpenCube is up to date (${current}).`, "✓")
|
|
87
|
+
|
|
88
|
+
return cubNotice(
|
|
89
|
+
[
|
|
90
|
+
`OpenCube ${latest.version} is available. Current version: ${current}.`,
|
|
91
|
+
`Upgrade with: opencode plugin ${pkg.name}@${latest.version} --global --force`,
|
|
92
|
+
`Then fully restart opencode and run /pet again.`,
|
|
93
|
+
].join("\n"),
|
|
94
|
+
"↻",
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
30
98
|
async function injectNotice(client, sessionID, text) {
|
|
31
99
|
if (!sessionID || !client?.session?.prompt) return
|
|
32
100
|
|
|
@@ -85,10 +153,18 @@ module.exports = {
|
|
|
85
153
|
template: "/pet_fancy_say_hello",
|
|
86
154
|
description: "Trigger a randomized light show on OpenCube's free faces.",
|
|
87
155
|
}
|
|
156
|
+
cfg.command.pet_update = {
|
|
157
|
+
template: "/pet_update",
|
|
158
|
+
description: "Check npm for a newer OpenCube version and show the upgrade command.",
|
|
159
|
+
}
|
|
160
|
+
cfg.command.pet_upgrade = {
|
|
161
|
+
template: "/pet_upgrade",
|
|
162
|
+
description: "Alias for /pet_update.",
|
|
163
|
+
}
|
|
88
164
|
},
|
|
89
165
|
|
|
90
166
|
"command.execute.before": async (input, output) => {
|
|
91
|
-
if (!["pet", "pet_stop", "pet_say_hello", "pet_fancy_say_hello"].includes(input.command)) return
|
|
167
|
+
if (!["pet", "pet_stop", "pet_say_hello", "pet_fancy_say_hello", "pet_update", "pet_upgrade"].includes(input.command)) return
|
|
92
168
|
|
|
93
169
|
// There is no official cancel primitive in command.execute.before yet.
|
|
94
170
|
// Throwing this sentinel aborts the command flow before opencode sends
|
|
@@ -129,6 +205,39 @@ module.exports = {
|
|
|
129
205
|
? cubNotice("OpenCube is putting on a light show ✨", "✺")
|
|
130
206
|
: cubNotice("OpenCube is sleeping... zzz Start it with /pet before the light show.", "☾"),
|
|
131
207
|
)
|
|
208
|
+
} else if (isUpdateCheck(input)) {
|
|
209
|
+
let latest
|
|
210
|
+
try {
|
|
211
|
+
latest = await fetchLatestPackageVersion()
|
|
212
|
+
const versionComparison = compareVersions(latest.version, pkg.version)
|
|
213
|
+
await sendEvent({
|
|
214
|
+
type: "update.check",
|
|
215
|
+
message: "OpenCube checked npm for updates",
|
|
216
|
+
package: pkg.name,
|
|
217
|
+
currentVersion: pkg.version,
|
|
218
|
+
latestVersion: latest.version,
|
|
219
|
+
updateAvailable: versionComparison > 0,
|
|
220
|
+
devVersion: versionComparison < 0,
|
|
221
|
+
tarball: latest.tarball,
|
|
222
|
+
timeoutMs: UPDATE_CHECK_TIMEOUT_MS,
|
|
223
|
+
sessionID: input.sessionID,
|
|
224
|
+
source: "opencube-plugin",
|
|
225
|
+
})
|
|
226
|
+
} catch (error) {
|
|
227
|
+
await sendEvent({
|
|
228
|
+
type: "update.check_failed",
|
|
229
|
+
message: "OpenCube could not check npm for updates",
|
|
230
|
+
package: pkg.name,
|
|
231
|
+
currentVersion: pkg.version,
|
|
232
|
+
error: error?.message || String(error),
|
|
233
|
+
sessionID: input.sessionID,
|
|
234
|
+
source: "opencube-plugin",
|
|
235
|
+
})
|
|
236
|
+
await injectNotice(client, input.sessionID, cubNotice(`Could not check npm for OpenCube updates: ${error?.message || error}`, "△"))
|
|
237
|
+
handled()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
await injectNotice(client, input.sessionID, formatUpdateNotice(latest))
|
|
132
241
|
} else {
|
|
133
242
|
await showPet({
|
|
134
243
|
onProgress: (message) => injectNotice(client, input.sessionID, message),
|
|
@@ -169,6 +278,45 @@ module.exports = {
|
|
|
169
278
|
return
|
|
170
279
|
}
|
|
171
280
|
|
|
281
|
+
if (event.type === "question.asked") {
|
|
282
|
+
const question = event.properties || {}
|
|
283
|
+
await sendEvent({
|
|
284
|
+
type: "question.ask",
|
|
285
|
+
message: "opencode is waiting for a question answer",
|
|
286
|
+
sessionID: question.sessionID,
|
|
287
|
+
requestID: question.id,
|
|
288
|
+
questions: question.questions,
|
|
289
|
+
tool: question.tool,
|
|
290
|
+
source: "opencube-plugin",
|
|
291
|
+
})
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (event.type === "question.replied") {
|
|
296
|
+
const question = event.properties || {}
|
|
297
|
+
await sendEvent({
|
|
298
|
+
type: "question.reply",
|
|
299
|
+
message: "opencode question was answered",
|
|
300
|
+
sessionID: question.sessionID,
|
|
301
|
+
requestID: question.requestID,
|
|
302
|
+
answers: question.answers,
|
|
303
|
+
source: "opencube-plugin",
|
|
304
|
+
})
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (event.type === "question.rejected") {
|
|
309
|
+
const question = event.properties || {}
|
|
310
|
+
await sendEvent({
|
|
311
|
+
type: "question.reject",
|
|
312
|
+
message: "opencode question was rejected",
|
|
313
|
+
sessionID: question.sessionID,
|
|
314
|
+
requestID: question.requestID,
|
|
315
|
+
source: "opencube-plugin",
|
|
316
|
+
})
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
172
320
|
if (event.type !== "session.status") return
|
|
173
321
|
|
|
174
322
|
const sessionID = event.properties?.sessionID
|