opencode-pixel-office 1.0.7 → 1.0.9
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 +3 -3
- package/bin/claude-code-hook.js +134 -0
- package/bin/opencode-pixel-office.js +143 -11
- package/client/dist/assets/{index-Cfnbdbzw.js → index-BYMX3DUt.js} +73 -73
- package/client/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugin/pixel-office.js +24 -2
- package/server/index.ts +1 -1
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ The system consists of three main parts:
|
|
|
15
15
|
|
|
16
16
|
```mermaid
|
|
17
17
|
graph TD
|
|
18
|
-
A[OpenCode IDE] -->|Plugin Events via HTTP| B(Pixel Office Server :
|
|
18
|
+
A[OpenCode IDE] -->|Plugin Events via HTTP| B(Pixel Office Server :3000)
|
|
19
19
|
B -->|Broadcast State via WebSocket| C[React Client]
|
|
20
20
|
C -->|Render| D[PixiJS Scene]
|
|
21
21
|
C -->|Render| E[HUD / Sidebar]
|
|
@@ -92,7 +92,7 @@ Monitor your agents from your phone or tablet!
|
|
|
92
92
|
This sets up the standalone app in `~/.opencode/pixel-office` and installs the `pixel-office.js` plugin script to `~/.opencode/plugins/`.
|
|
93
93
|
|
|
94
94
|
3. **Start OpenCode**:
|
|
95
|
-
Simply open your IDE. Pixel Office will auto-launch in your browser at `http://localhost:
|
|
95
|
+
Simply open your IDE. Pixel Office will auto-launch in your browser at `http://localhost:3000`.
|
|
96
96
|
|
|
97
97
|
### CLI Commands
|
|
98
98
|
|
|
@@ -114,7 +114,7 @@ npm install
|
|
|
114
114
|
#### 2. Start the Server (Dev Mode)
|
|
115
115
|
```bash
|
|
116
116
|
npm start
|
|
117
|
-
# Server runs on http://localhost:
|
|
117
|
+
# Server runs on http://localhost:3000, watching for changes
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
#### 3. Start the Client (Dev Mode)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
|
|
6
|
+
const readStdin = async () => {
|
|
7
|
+
const chunks = [];
|
|
8
|
+
for await (const chunk of process.stdin) {
|
|
9
|
+
chunks.push(chunk);
|
|
10
|
+
}
|
|
11
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const postEvent = async (endpoint, event) => {
|
|
15
|
+
try {
|
|
16
|
+
await fetch(endpoint, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/json" },
|
|
19
|
+
body: JSON.stringify(event),
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
const logPath = path.join(os.homedir(), ".claude", "pixel-office-hook.log");
|
|
23
|
+
fs.appendFileSync(logPath, `${new Date().toISOString()} ${String(error)}\n`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const readMode = () => {
|
|
28
|
+
try {
|
|
29
|
+
const configPath = path.join(os.homedir(), ".opencode", "pixel-office", "config.json");
|
|
30
|
+
if (fs.existsSync(configPath)) {
|
|
31
|
+
const data = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
32
|
+
return data.mode || "opencode";
|
|
33
|
+
}
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return "opencode";
|
|
36
|
+
}
|
|
37
|
+
return "opencode";
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const mapHookToEvent = (input) => {
|
|
41
|
+
const hook = input.hook_event_name || "";
|
|
42
|
+
const sessionId = input.session_id || "";
|
|
43
|
+
const cwd = input.cwd || "";
|
|
44
|
+
const toolName = input.tool_name || "";
|
|
45
|
+
const toolInput = input.tool_input || {};
|
|
46
|
+
const prompt = input.prompt || input.user_prompt || "";
|
|
47
|
+
const permission = input.permission || {};
|
|
48
|
+
|
|
49
|
+
const info = {
|
|
50
|
+
id: sessionId || `claude-${Date.now()}`,
|
|
51
|
+
sessionID: sessionId || "",
|
|
52
|
+
role: "user",
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
switch (hook) {
|
|
56
|
+
case "SessionStart":
|
|
57
|
+
return {
|
|
58
|
+
type: "session.created",
|
|
59
|
+
properties: { info: { id: sessionId, title: cwd } },
|
|
60
|
+
};
|
|
61
|
+
case "SessionEnd":
|
|
62
|
+
return {
|
|
63
|
+
type: "session.deleted",
|
|
64
|
+
properties: { sessionID: sessionId },
|
|
65
|
+
};
|
|
66
|
+
case "UserPromptSubmit":
|
|
67
|
+
return {
|
|
68
|
+
type: "message.updated",
|
|
69
|
+
properties: {
|
|
70
|
+
info,
|
|
71
|
+
message: { content: prompt },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
case "PreToolUse":
|
|
75
|
+
return {
|
|
76
|
+
type: "tool.execute.before",
|
|
77
|
+
properties: { tool: { name: toolName, input: toolInput } },
|
|
78
|
+
};
|
|
79
|
+
case "PostToolUse":
|
|
80
|
+
case "PostToolUseFailure":
|
|
81
|
+
return {
|
|
82
|
+
type: "tool.execute.after",
|
|
83
|
+
properties: { tool: { name: toolName, input: toolInput } },
|
|
84
|
+
};
|
|
85
|
+
case "PermissionRequest":
|
|
86
|
+
return {
|
|
87
|
+
type: "permission.asked",
|
|
88
|
+
properties: { permission },
|
|
89
|
+
};
|
|
90
|
+
case "Notification":
|
|
91
|
+
return {
|
|
92
|
+
type: "tui.toast.show",
|
|
93
|
+
properties: { message: input.message || "" },
|
|
94
|
+
};
|
|
95
|
+
case "PreCompact":
|
|
96
|
+
return {
|
|
97
|
+
type: "session.compacted",
|
|
98
|
+
properties: { info: { id: sessionId } },
|
|
99
|
+
};
|
|
100
|
+
case "Stop":
|
|
101
|
+
return {
|
|
102
|
+
type: "session.status",
|
|
103
|
+
properties: { sessionID: sessionId, status: { type: "idle" } },
|
|
104
|
+
};
|
|
105
|
+
case "SubagentStart":
|
|
106
|
+
case "SubagentStop":
|
|
107
|
+
return {
|
|
108
|
+
type: "session.updated",
|
|
109
|
+
properties: { info: { id: sessionId } },
|
|
110
|
+
};
|
|
111
|
+
default:
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const main = async () => {
|
|
117
|
+
const raw = await readStdin();
|
|
118
|
+
if (!raw) {
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
const input = JSON.parse(raw);
|
|
122
|
+
const endpoint = process.env.PIXEL_OFFICE_URL || "http://localhost:3000/events";
|
|
123
|
+
if (readMode() !== "claude-code") {
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
const event = mapHookToEvent(input);
|
|
127
|
+
if (!event) {
|
|
128
|
+
process.exit(0);
|
|
129
|
+
}
|
|
130
|
+
await postEvent(endpoint, event);
|
|
131
|
+
process.exit(0);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
main().catch(() => process.exit(0));
|
|
@@ -17,8 +17,12 @@ const DEFAULT_CONFIG_PATH = path.join(os.homedir(), ".config", "opencode", "open
|
|
|
17
17
|
const PIXEL_OFFICE_CONFIG_PATH = path.join(DEFAULT_APP_DIR, "config.json");
|
|
18
18
|
|
|
19
19
|
const args = process.argv.slice(2);
|
|
20
|
-
const shouldInstall = args.includes("install");
|
|
20
|
+
const shouldInstall = args.includes("install") && !args.includes("claude-code") && !args.includes("switch");
|
|
21
|
+
const shouldInstallClaude = args[0] === "claude-code" && args[1] === "install";
|
|
22
|
+
const shouldSwitch = args[0] === "switch";
|
|
23
|
+
const switchTarget = shouldSwitch ? args[1] : null;
|
|
21
24
|
const shouldUninstall = args.includes("uninstall");
|
|
25
|
+
const shouldStart = args[0] === "start";
|
|
22
26
|
const yesFlag = args.includes("--yes") || args.includes("-y");
|
|
23
27
|
const skipJson = args.includes("--no-json");
|
|
24
28
|
const portIndex = args.findIndex((arg) => arg === "--port");
|
|
@@ -28,14 +32,62 @@ const printHelp = () => {
|
|
|
28
32
|
console.log("opencode-pixel-office installer\n");
|
|
29
33
|
console.log("Usage:");
|
|
30
34
|
console.log(" opencode-pixel-office install [--yes] [--port <number>]");
|
|
35
|
+
console.log(" opencode-pixel-office claude-code install");
|
|
36
|
+
console.log(" opencode-pixel-office switch <opencode|claude-code>");
|
|
37
|
+
console.log(" opencode-pixel-office start");
|
|
31
38
|
console.log(" opencode-pixel-office uninstall");
|
|
32
39
|
console.log(" opencode-pixel-office stop");
|
|
33
40
|
console.log("\nOptions:");
|
|
34
|
-
console.log(" --port <number> Configure the server port (default:
|
|
41
|
+
console.log(" --port <number> Configure the server port (default: 3000)");
|
|
35
42
|
console.log(" --no-json Skip updating opencode.json");
|
|
36
43
|
console.log(" --yes, -y Overwrite without prompting");
|
|
37
44
|
};
|
|
38
45
|
|
|
46
|
+
const installClaudeCodeHooks = () => {
|
|
47
|
+
const claudeDir = path.join(os.homedir(), ".claude");
|
|
48
|
+
const hookDir = path.join(claudeDir, "hooks");
|
|
49
|
+
const settingsPath = path.join(claudeDir, "settings.json");
|
|
50
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
51
|
+
|
|
52
|
+
const hookSource = path.resolve(__dirname, "claude-code-hook.js");
|
|
53
|
+
const hookTarget = path.join(hookDir, "opencode-pixel-office-hook.js");
|
|
54
|
+
fs.copyFileSync(hookSource, hookTarget);
|
|
55
|
+
|
|
56
|
+
const hookCommand = `node ${hookTarget}`;
|
|
57
|
+
const hooksConfig = {
|
|
58
|
+
SessionStart: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
59
|
+
UserPromptSubmit: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
60
|
+
PreToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
61
|
+
PostToolUse: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
62
|
+
PostToolUseFailure: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
63
|
+
PermissionRequest: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
64
|
+
Notification: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
65
|
+
SubagentStart: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
66
|
+
SubagentStop: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
67
|
+
Stop: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
68
|
+
PreCompact: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
69
|
+
SessionEnd: [{ matcher: "", hooks: [{ type: "command", command: hookCommand }] }],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let settings = { hooks: {} };
|
|
73
|
+
if (fs.existsSync(settingsPath)) {
|
|
74
|
+
try {
|
|
75
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
|
|
76
|
+
} catch {
|
|
77
|
+
settings = { hooks: {} };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
settings.hooks = settings.hooks || {};
|
|
82
|
+
Object.entries(hooksConfig).forEach(([eventName, value]) => {
|
|
83
|
+
settings.hooks[eventName] = value;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
87
|
+
console.log(`✓ Installed Claude Code hooks into ${settingsPath}`);
|
|
88
|
+
console.log("Claude Code will now stream events to Pixel Office.");
|
|
89
|
+
};
|
|
90
|
+
|
|
39
91
|
const loadConfig = () => {
|
|
40
92
|
try {
|
|
41
93
|
if (fs.existsSync(PIXEL_OFFICE_CONFIG_PATH)) {
|
|
@@ -45,6 +97,11 @@ const loadConfig = () => {
|
|
|
45
97
|
return {};
|
|
46
98
|
};
|
|
47
99
|
|
|
100
|
+
const saveConfig = (config) => {
|
|
101
|
+
fs.mkdirSync(DEFAULT_APP_DIR, { recursive: true });
|
|
102
|
+
fs.writeFileSync(PIXEL_OFFICE_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
|
|
103
|
+
};
|
|
104
|
+
|
|
48
105
|
const prompt = async (question) => {
|
|
49
106
|
const rl = readline.createInterface({
|
|
50
107
|
input: process.stdin,
|
|
@@ -75,6 +132,23 @@ const copyRecursiveSync = (src, dest) => {
|
|
|
75
132
|
};
|
|
76
133
|
|
|
77
134
|
const run = async () => {
|
|
135
|
+
if (shouldSwitch) {
|
|
136
|
+
if (switchTarget !== "opencode" && switchTarget !== "claude-code") {
|
|
137
|
+
console.error("Switch target must be 'opencode' or 'claude-code'.");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
const config = loadConfig();
|
|
141
|
+
config.mode = switchTarget;
|
|
142
|
+
saveConfig(config);
|
|
143
|
+
console.log(`✓ Mode set to ${switchTarget}`);
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (shouldInstallClaude) {
|
|
148
|
+
installClaudeCodeHooks();
|
|
149
|
+
process.exit(0);
|
|
150
|
+
}
|
|
151
|
+
|
|
78
152
|
if (shouldUninstall) {
|
|
79
153
|
const targetPluginPath = path.join(DEFAULT_PLUGIN_DIR, PLUGIN_NAME);
|
|
80
154
|
|
|
@@ -116,9 +190,69 @@ const run = async () => {
|
|
|
116
190
|
process.exit(0);
|
|
117
191
|
}
|
|
118
192
|
|
|
193
|
+
if (shouldStart) {
|
|
194
|
+
const config = loadConfig();
|
|
195
|
+
const port = portArg ? parseInt(portArg, 10) : (config.port || 3000);
|
|
196
|
+
const serverScript = "server/index.ts";
|
|
197
|
+
|
|
198
|
+
const rootSource = path.resolve(__dirname, "..");
|
|
199
|
+
const localServerPath = path.join(rootSource, "server", "index.ts");
|
|
200
|
+
const globalServerPath = path.join(DEFAULT_APP_DIR, "server", "index.ts");
|
|
201
|
+
|
|
202
|
+
let serverCwd;
|
|
203
|
+
let useGlobal = false;
|
|
204
|
+
if (fs.existsSync(localServerPath)) {
|
|
205
|
+
serverCwd = rootSource;
|
|
206
|
+
} else if (fs.existsSync(globalServerPath)) {
|
|
207
|
+
serverCwd = DEFAULT_APP_DIR;
|
|
208
|
+
useGlobal = true;
|
|
209
|
+
} else {
|
|
210
|
+
console.error("Server not found. Run 'opencode-pixel-office install' first.");
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const pidOutput = execSync(`lsof -t -i :${port} 2>/dev/null`).toString().trim();
|
|
216
|
+
if (pidOutput) {
|
|
217
|
+
console.log(`Pixel Office is already running on port ${port} (PID: ${pidOutput})`);
|
|
218
|
+
const url = `http://localhost:${port}`;
|
|
219
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
220
|
+
try { execSync(`${openCmd} ${url}`); } catch { }
|
|
221
|
+
console.log(`Opened ${url}`);
|
|
222
|
+
process.exit(0);
|
|
223
|
+
}
|
|
224
|
+
} catch { }
|
|
225
|
+
|
|
226
|
+
let tsxBin = "tsx";
|
|
227
|
+
if (useGlobal) {
|
|
228
|
+
const globalTsx = path.join(serverCwd, "node_modules", ".bin", "tsx");
|
|
229
|
+
if (fs.existsSync(globalTsx)) {
|
|
230
|
+
tsxBin = globalTsx;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const { spawn } = await import("node:child_process");
|
|
235
|
+
const child = spawn(tsxBin, [serverScript], {
|
|
236
|
+
cwd: serverCwd,
|
|
237
|
+
env: { ...process.env, PORT: String(port) },
|
|
238
|
+
detached: true,
|
|
239
|
+
stdio: "ignore",
|
|
240
|
+
});
|
|
241
|
+
child.unref();
|
|
242
|
+
|
|
243
|
+
console.log(`Pixel Office server started on port ${port} (PID: ${child.pid})`);
|
|
244
|
+
|
|
245
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
246
|
+
const url = `http://localhost:${port}`;
|
|
247
|
+
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
248
|
+
try { execSync(`${openCmd} ${url}`); } catch { }
|
|
249
|
+
console.log(`Opened ${url}`);
|
|
250
|
+
process.exit(0);
|
|
251
|
+
}
|
|
252
|
+
|
|
119
253
|
if (args.includes("stop")) {
|
|
120
254
|
const config = loadConfig();
|
|
121
|
-
const port = config.port ||
|
|
255
|
+
const port = config.port || 3000;
|
|
122
256
|
|
|
123
257
|
console.log(`Stopping Pixel Office server on port ${port}...`);
|
|
124
258
|
try {
|
|
@@ -198,19 +332,20 @@ const run = async () => {
|
|
|
198
332
|
fs.copyFileSync(path.join(rootSource, "package.json"), path.join(DEFAULT_APP_DIR, "package.json"));
|
|
199
333
|
|
|
200
334
|
// Save Port Config if specified
|
|
335
|
+
const configData = loadConfig();
|
|
201
336
|
if (portArg) {
|
|
202
337
|
const port = parseInt(portArg, 10);
|
|
203
338
|
if (!isNaN(port)) {
|
|
204
|
-
|
|
205
|
-
fs.writeFileSync(PIXEL_OFFICE_CONFIG_PATH, JSON.stringify(configData, null, 2), "utf8");
|
|
339
|
+
configData.port = port;
|
|
206
340
|
console.log(` - Saved port configuration: ${port}`);
|
|
207
341
|
} else {
|
|
208
342
|
console.warn(" ! Invalid port number provided. Using default.");
|
|
209
343
|
}
|
|
210
|
-
} else if (!fs.existsSync(PIXEL_OFFICE_CONFIG_PATH)) {
|
|
211
|
-
// Ensure a default config exists or verify if we need one?
|
|
212
|
-
// For now, let's leave it minimal. The plugin defaults to 5100.
|
|
213
344
|
}
|
|
345
|
+
if (!configData.mode) {
|
|
346
|
+
configData.mode = "opencode";
|
|
347
|
+
}
|
|
348
|
+
saveConfig(configData);
|
|
214
349
|
|
|
215
350
|
// npm install
|
|
216
351
|
console.log(" - Installing production dependencies...");
|
|
@@ -232,6 +367,3 @@ run().catch((error) => {
|
|
|
232
367
|
console.error("Installer failed:", error);
|
|
233
368
|
process.exit(1);
|
|
234
369
|
});
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|