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 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 :5100)
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:5100`.
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:5100, watching for changes
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: 5100)");
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 || 5100;
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
- const configData = { port };
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
-