opencube 0.1.0

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 ADDED
@@ -0,0 +1,106 @@
1
+ <p align="center">
2
+ <img src="assets/opencode-icon.png" width="96" height="96" alt="OpenCub icon" />
3
+ </p>
4
+
5
+ # OpenCub
6
+
7
+ OpenCub is a tiny desktop pet for [opencode](https://opencode.ai/).
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
+ - idle sessions release their face glow after the cube slows down
13
+ - `/pet_say_hello` flashes one free face
14
+ - `/pet_fancy_say_hello` plays a randomized light show on free faces
15
+
16
+ OpenCub is packaged as an opencode plugin plus an Electron desktop process.
17
+
18
+ ## Install
19
+
20
+ > OpenCub is not published yet. These are the intended install commands once published.
21
+
22
+ Install globally through opencode:
23
+
24
+ ```sh
25
+ opencode plugin opencub --global
26
+ ```
27
+
28
+ Then restart opencode and run:
29
+
30
+ ```text
31
+ /pet
32
+ ```
33
+
34
+ You can also add it manually to `~/.config/opencode/opencode.json`:
35
+
36
+ ```json
37
+ {
38
+ "plugin": ["opencub"]
39
+ }
40
+ ```
41
+
42
+ ## Commands
43
+
44
+ | Command | Description |
45
+ | --- | --- |
46
+ | `/pet` | Show or start OpenCub. |
47
+ | `/pet_stop` | Quit OpenCub. |
48
+ | `/pet_say_hello` | Flash one currently free face three times with a random color. |
49
+ | `/pet_fancy_say_hello` | Run a denser randomized light show across currently free faces. |
50
+
51
+ These commands are handled by the plugin and do not get sent to the model.
52
+
53
+ ## How it works
54
+
55
+ OpenCub has two parts in one npm package:
56
+
57
+ 1. `src/plugin-server.cjs` — the opencode plugin entrypoint.
58
+ 2. `src/main.js` — the Electron desktop pet.
59
+
60
+ The plugin registers slash commands, listens for opencode `session.status` events, and sends events to the desktop pet over a local HTTP API:
61
+
62
+ ```text
63
+ opencode plugin -> http://127.0.0.1:47832 -> Electron OpenCub
64
+ ```
65
+
66
+ The Electron process owns the window, Three.js renderer, cube rotation, face glow state, and inbox/debug endpoints.
67
+
68
+ ## Requirements
69
+
70
+ - opencode
71
+ - macOS, Windows, or Linux capable of running Electron
72
+ - network access to install npm dependencies during first plugin install
73
+
74
+ Users do not need to run `npm install` manually when installing via `opencode plugin`. opencode installs npm plugins and dependencies automatically.
75
+
76
+ ## Notes
77
+
78
+ - The first install may take a while because Electron is downloaded as a runtime dependency.
79
+ - If commands do not appear after installation, restart opencode.
80
+ - OpenCub uses a local-only HTTP server on `127.0.0.1:47832`.
81
+ - If that port is already in use, set `OPENCODE_PET_PORT` before starting opencode.
82
+
83
+ ## Local development
84
+
85
+ From this repository:
86
+
87
+ ```sh
88
+ npm install
89
+ npm run start
90
+ ```
91
+
92
+ For local opencode plugin testing, point your opencode config at the package directory:
93
+
94
+ ```json
95
+ {
96
+ "plugin": ["/path/to/opencub"]
97
+ }
98
+ ```
99
+
100
+ After changing plugin files or opencode config, restart opencode.
101
+
102
+ To inspect the package contents before publishing:
103
+
104
+ ```sh
105
+ npm pack --dry-run
106
+ ```
@@ -0,0 +1,168 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>opencode icon 3D three.js preview</title>
7
+ <style>
8
+ html, body {
9
+ margin: 0;
10
+ width: 100%;
11
+ height: 100%;
12
+ overflow: hidden;
13
+ background: #ecece7;
14
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
15
+ }
16
+ #app { width: 100%; height: 100%; }
17
+ .hud {
18
+ position: fixed;
19
+ left: 20px;
20
+ top: 18px;
21
+ padding: 12px 14px;
22
+ border: 2px solid rgba(0,0,0,.16);
23
+ border-radius: 14px;
24
+ background: rgba(255,255,255,.72);
25
+ backdrop-filter: blur(8px);
26
+ color: #111;
27
+ font-size: 13px;
28
+ line-height: 1.55;
29
+ box-shadow: 0 12px 36px rgba(0,0,0,.08);
30
+ user-select: none;
31
+ }
32
+ .hud b { font-size: 15px; }
33
+ .hud .muted { color: rgba(0,0,0,.58); }
34
+ </style>
35
+ </head>
36
+ <body>
37
+ <div id="app"></div>
38
+ <div class="hud">
39
+ <b>3D opencode icon</b><br />
40
+ <span class="muted">只用六张原生 opencode icon 贴图围成立方体</span><br />
41
+ 拖动旋转 · 滚轮缩放 · idle 可作为正面静止态
42
+ </div>
43
+
44
+ <script type="module">
45
+ import * as THREE from "https://esm.sh/three@0.164.1"
46
+ import { OrbitControls } from "https://esm.sh/three@0.164.1/examples/jsm/controls/OrbitControls.js"
47
+
48
+ const container = document.getElementById("app")
49
+ const scene = new THREE.Scene()
50
+ scene.background = new THREE.Color(0xecece7)
51
+
52
+ const camera = new THREE.PerspectiveCamera(38, window.innerWidth / window.innerHeight, 0.1, 100)
53
+ camera.position.set(4.3, 3.2, 6.4)
54
+
55
+ const renderer = new THREE.WebGLRenderer({ antialias: true })
56
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2))
57
+ renderer.setSize(window.innerWidth, window.innerHeight)
58
+ renderer.shadowMap.enabled = true
59
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap
60
+ container.appendChild(renderer.domElement)
61
+
62
+ const controls = new OrbitControls(camera, renderer.domElement)
63
+ controls.enableDamping = true
64
+ controls.dampingFactor = 0.08
65
+ controls.target.set(0, 0.08, 0)
66
+ controls.minDistance = 3.4
67
+ controls.maxDistance = 12
68
+
69
+ const group = new THREE.Group()
70
+ scene.add(group)
71
+
72
+ const iconTexture = new THREE.TextureLoader().load("./opencode-icon.png", (texture) => {
73
+ texture.colorSpace = THREE.SRGBColorSpace
74
+ texture.anisotropy = renderer.capabilities.getMaxAnisotropy()
75
+ texture.needsUpdate = true
76
+ })
77
+ const iconFaceMaterial = new THREE.MeshStandardMaterial({
78
+ map: iconTexture,
79
+ roughness: 0.48,
80
+ metalness: 0.02,
81
+ transparent: true,
82
+ alphaTest: 0.02,
83
+ side: THREE.DoubleSide,
84
+ })
85
+
86
+ // 只用 6 张 PlaneGeometry 手动拼 cube:没有中间黑盒子。
87
+ // 原生 PNG 自带圆角/透明边缘,所以圆角处会自然镂空,
88
+ // 看起来更像几张 icon 面片围成的立方体。
89
+ // PlaneGeometry 默认只有正面可见;这里用 DoubleSide,
90
+ // 否则透过圆角看进去时,看不到其它 icon 面片的背面。
91
+ const size = 3.2
92
+ const half = size / 2
93
+
94
+ function addIconFace(name, position, rotation) {
95
+ const face = new THREE.Mesh(new THREE.PlaneGeometry(size, size), iconFaceMaterial.clone())
96
+ face.name = name
97
+ face.position.set(...position)
98
+ face.rotation.set(...rotation)
99
+ face.castShadow = true
100
+ face.receiveShadow = true
101
+ group.add(face)
102
+ return face
103
+ }
104
+
105
+ addIconFace("front", [0, 0, half + 0.001], [0, 0, 0])
106
+ addIconFace("back", [0, 0, -half - 0.001], [0, Math.PI, 0])
107
+ addIconFace("right", [half + 0.001, 0, 0], [0, Math.PI / 2, 0])
108
+ addIconFace("left", [-half - 0.001, 0, 0], [0, -Math.PI / 2, 0])
109
+ addIconFace("top", [0, half + 0.001, 0], [-Math.PI / 2, 0, 0])
110
+ addIconFace("bottom", [0, -half - 0.001, 0], [Math.PI / 2, 0, 0])
111
+
112
+ group.rotation.x = -0.08
113
+ group.rotation.y = -0.38
114
+
115
+ const hemi = new THREE.HemisphereLight(0xffffff, 0xb8b8b2, 1.45)
116
+ scene.add(hemi)
117
+
118
+ const key = new THREE.DirectionalLight(0xffffff, 3.2)
119
+ key.position.set(4, 6, 5)
120
+ key.castShadow = true
121
+ key.shadow.mapSize.set(2048, 2048)
122
+ key.shadow.camera.near = 0.5
123
+ key.shadow.camera.far = 20
124
+ key.shadow.camera.left = -6
125
+ key.shadow.camera.right = 6
126
+ key.shadow.camera.top = 6
127
+ key.shadow.camera.bottom = -6
128
+ scene.add(key)
129
+
130
+ const fill = new THREE.DirectionalLight(0xfff1df, 1.0)
131
+ fill.position.set(-4, 2.5, 3)
132
+ scene.add(fill)
133
+
134
+ const rim = new THREE.DirectionalLight(0xffffff, 1.2)
135
+ rim.position.set(-3, 5, -4)
136
+ scene.add(rim)
137
+
138
+ const floor = new THREE.Mesh(
139
+ new THREE.CircleGeometry(4.6, 96),
140
+ new THREE.ShadowMaterial({ opacity: 0.18 }),
141
+ )
142
+ floor.rotation.x = -Math.PI / 2
143
+ floor.position.y = -2.25
144
+ floor.receiveShadow = true
145
+ scene.add(floor)
146
+
147
+ let autoSpin = true
148
+ renderer.domElement.addEventListener("pointerdown", () => {
149
+ autoSpin = false
150
+ })
151
+
152
+ function resize() {
153
+ camera.aspect = window.innerWidth / window.innerHeight
154
+ camera.updateProjectionMatrix()
155
+ renderer.setSize(window.innerWidth, window.innerHeight)
156
+ }
157
+ window.addEventListener("resize", resize)
158
+
159
+ function animate() {
160
+ requestAnimationFrame(animate)
161
+ if (autoSpin) group.rotation.y += 0.008
162
+ controls.update()
163
+ renderer.render(scene, camera)
164
+ }
165
+ animate()
166
+ </script>
167
+ </body>
168
+ </html>
Binary file
@@ -0,0 +1,99 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="720" height="520" viewBox="0 0 720 520" role="img" aria-label="opencode pet 3D busy spin concept">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
4
+ <stop offset="0" stop-color="#f7f7f5"/>
5
+ <stop offset="1" stop-color="#e8e8e3"/>
6
+ </linearGradient>
7
+ <linearGradient id="side" x1="0" y1="0" x2="1" y2="0">
8
+ <stop offset="0" stop-color="#050505"/>
9
+ <stop offset="1" stop-color="#1d1d1d"/>
10
+ </linearGradient>
11
+ <filter id="softShadow" x="-50%" y="-50%" width="200%" height="200%">
12
+ <feGaussianBlur in="SourceAlpha" stdDeviation="7"/>
13
+ <feOffset dx="0" dy="8" result="offset"/>
14
+ <feComponentTransfer>
15
+ <feFuncA type="linear" slope="0.25"/>
16
+ </feComponentTransfer>
17
+ <feMerge>
18
+ <feMergeNode/>
19
+ <feMergeNode in="SourceGraphic"/>
20
+ </feMerge>
21
+ </filter>
22
+ <marker id="arrow" markerWidth="10" markerHeight="10" refX="7" refY="3" orient="auto" markerUnits="strokeWidth">
23
+ <path d="M0,0 L0,6 L8,3 z" fill="#ff5d73"/>
24
+ </marker>
25
+ </defs>
26
+
27
+ <rect width="720" height="520" fill="url(#bg)"/>
28
+
29
+ <text x="40" y="52" fill="#111" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="22" font-weight="700">busy 状态:opencode 小人 3D 转身</text>
30
+ <text x="40" y="82" fill="#555" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="14">任何 session busy → 绕竖直 Y 轴持续旋转;全部 idle → 停回正面</text>
31
+
32
+ <!-- 3D floor / orbit ring -->
33
+ <ellipse cx="360" cy="382" rx="172" ry="48" fill="#d7d7d0" opacity="0.8"/>
34
+ <ellipse cx="360" cy="382" rx="132" ry="34" fill="none" stroke="#ff5d73" stroke-width="5" stroke-dasharray="18 10" opacity="0.85"/>
35
+ <path d="M 248 376 C 286 333, 423 321, 492 363" fill="none" stroke="#ff5d73" stroke-width="6" marker-end="url(#arrow)" opacity="0.9"/>
36
+
37
+ <!-- vertical Y axis -->
38
+ <line x1="360" y1="110" x2="360" y2="405" stroke="#111" stroke-width="3" stroke-dasharray="8 8" opacity="0.35"/>
39
+ <text x="374" y="124" fill="#111" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="13">Y 轴</text>
40
+
41
+ <!-- ghost frames showing rotation -->
42
+ <g opacity="0.18" transform="translate(218 212) skewY(-8) scale(.72 1)">
43
+ <rect x="0" y="0" width="84" height="120" fill="#050505"/>
44
+ <rect x="14" y="20" width="56" height="72" fill="#f6f4ec"/>
45
+ <rect x="30" y="36" width="24" height="44" fill="#050505"/>
46
+ </g>
47
+ <g opacity="0.22" transform="translate(450 212) skewY(8) scale(.72 1)">
48
+ <rect x="0" y="0" width="84" height="120" fill="#050505"/>
49
+ <rect x="14" y="20" width="56" height="72" fill="#f6f4ec"/>
50
+ <rect x="30" y="36" width="24" height="44" fill="#050505"/>
51
+ </g>
52
+
53
+ <!-- main 3D pet body -->
54
+ <g filter="url(#softShadow)">
55
+ <!-- back/side depth -->
56
+ <polygon points="306,159 432,159 458,184 458,340 432,360 306,360 282,338 282,184" fill="#030303"/>
57
+ <polygon points="432,159 458,184 458,340 432,360 432,159" fill="url(#side)"/>
58
+ <polygon points="306,159 432,159 458,184 330,184" fill="#242424"/>
59
+
60
+ <!-- ears / side blocks -->
61
+ <rect x="260" y="224" width="34" height="72" fill="#050505"/>
62
+ <rect x="268" y="236" width="18" height="48" fill="#242424"/>
63
+ <rect x="448" y="224" width="34" height="72" fill="#050505"/>
64
+ <rect x="448" y="236" width="18" height="48" fill="#202020"/>
65
+
66
+ <!-- front face -->
67
+ <rect x="306" y="176" width="126" height="164" fill="#050505"/>
68
+ <rect x="318" y="188" width="102" height="22" fill="#252525"/>
69
+ <rect x="318" y="316" width="102" height="18" fill="#171717"/>
70
+
71
+ <!-- opencode white plate: deliberately vertical rectangle -->
72
+ <rect x="324" y="216" width="88" height="104" fill="#2d2d2d"/>
73
+ <rect x="330" y="222" width="76" height="92" fill="#d8d5cc"/>
74
+ <rect x="338" y="230" width="60" height="76" fill="#ffffff"/>
75
+ <rect x="354" y="248" width="30" height="50" fill="#050505"/>
76
+ <rect x="354" y="248" width="30" height="13" fill="#111"/>
77
+
78
+ <!-- arms / hands -->
79
+ <rect x="236" y="308" width="54" height="18" fill="#050505"/>
80
+ <rect x="226" y="298" width="26" height="26" fill="#050505"/>
81
+ <rect x="232" y="303" width="16" height="11" fill="#303030"/>
82
+ <rect x="444" y="308" width="54" height="18" fill="#050505"/>
83
+ <rect x="484" y="298" width="26" height="26" fill="#050505"/>
84
+ <rect x="486" y="303" width="16" height="11" fill="#303030"/>
85
+
86
+ <!-- feet -->
87
+ <rect x="318" y="356" width="32" height="32" fill="#050505"/>
88
+ <rect x="390" y="356" width="32" height="32" fill="#050505"/>
89
+ <rect x="326" y="356" width="22" height="10" fill="#242424"/>
90
+ <rect x="398" y="356" width="22" height="10" fill="#242424"/>
91
+ </g>
92
+
93
+ <!-- notes -->
94
+ <g font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="13" fill="#333">
95
+ <text x="72" y="438">idle:正面静止</text>
96
+ <text x="72" y="462">busy:scaleX + skew + 明暗面,模拟 3D 绕 Y 轴旋转</text>
97
+ <text x="72" y="486">不再需要球,busy 反馈更简单直接</text>
98
+ </g>
99
+ </svg>