opencode-pixel-office 1.0.8 → 1.0.10

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.
@@ -48,15 +48,17 @@ const mapHookToEvent = (input) => {
48
48
 
49
49
  const info = {
50
50
  id: sessionId || `claude-${Date.now()}`,
51
- sessionID: sessionId || "",
52
- role: "user",
51
+ sessionID: sessionId,
52
+ agent: "Claude",
53
+ title: cwd || "Claude Code",
54
+ model: { modelID: "claude", providerID: "anthropic" },
53
55
  };
54
56
 
55
57
  switch (hook) {
56
58
  case "SessionStart":
57
59
  return {
58
60
  type: "session.created",
59
- properties: { info: { id: sessionId, title: cwd } },
61
+ properties: { info },
60
62
  };
61
63
  case "SessionEnd":
62
64
  return {
@@ -67,35 +69,35 @@ const mapHookToEvent = (input) => {
67
69
  return {
68
70
  type: "message.updated",
69
71
  properties: {
70
- info,
71
- message: { content: prompt },
72
+ info: { ...info, role: "user" },
73
+ message: { content: prompt, role: "user" },
72
74
  },
73
75
  };
74
76
  case "PreToolUse":
75
77
  return {
76
78
  type: "tool.execute.before",
77
- properties: { tool: { name: toolName, input: toolInput } },
79
+ properties: { info, tool: { name: toolName, input: toolInput } },
78
80
  };
79
81
  case "PostToolUse":
80
82
  case "PostToolUseFailure":
81
83
  return {
82
84
  type: "tool.execute.after",
83
- properties: { tool: { name: toolName, input: toolInput } },
85
+ properties: { info, tool: { name: toolName, input: toolInput } },
84
86
  };
85
87
  case "PermissionRequest":
86
88
  return {
87
89
  type: "permission.asked",
88
- properties: { permission },
90
+ properties: { info, permission },
89
91
  };
90
92
  case "Notification":
91
93
  return {
92
94
  type: "tui.toast.show",
93
- properties: { message: input.message || "" },
95
+ properties: { info, message: input.message || "" },
94
96
  };
95
97
  case "PreCompact":
96
98
  return {
97
99
  type: "session.compacted",
98
- properties: { info: { id: sessionId } },
100
+ properties: { info },
99
101
  };
100
102
  case "Stop":
101
103
  return {
@@ -106,7 +108,7 @@ const mapHookToEvent = (input) => {
106
108
  case "SubagentStop":
107
109
  return {
108
110
  type: "session.updated",
109
- properties: { info: { id: sessionId } },
111
+ properties: { info },
110
112
  };
111
113
  default:
112
114
  return null;
@@ -22,23 +22,37 @@ const shouldInstallClaude = args[0] === "claude-code" && args[1] === "install";
22
22
  const shouldSwitch = args[0] === "switch";
23
23
  const switchTarget = shouldSwitch ? args[1] : null;
24
24
  const shouldUninstall = args.includes("uninstall");
25
+ const shouldStart = args[0] === "start";
26
+ const shouldVersion = args.includes("--version") || args.includes("-v");
25
27
  const yesFlag = args.includes("--yes") || args.includes("-y");
26
28
  const skipJson = args.includes("--no-json");
27
29
  const portIndex = args.findIndex((arg) => arg === "--port");
28
30
  const portArg = portIndex !== -1 ? args[portIndex + 1] : null;
29
31
 
32
+ const getVersion = () => {
33
+ try {
34
+ const pkgPath = path.resolve(__dirname, "..", "package.json");
35
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
36
+ return pkg.version || "unknown";
37
+ } catch {
38
+ return "unknown";
39
+ }
40
+ };
41
+
30
42
  const printHelp = () => {
31
43
  console.log("opencode-pixel-office installer\n");
32
44
  console.log("Usage:");
33
45
  console.log(" opencode-pixel-office install [--yes] [--port <number>]");
34
46
  console.log(" opencode-pixel-office claude-code install");
35
47
  console.log(" opencode-pixel-office switch <opencode|claude-code>");
48
+ console.log(" opencode-pixel-office start");
36
49
  console.log(" opencode-pixel-office uninstall");
37
50
  console.log(" opencode-pixel-office stop");
38
51
  console.log("\nOptions:");
39
52
  console.log(" --port <number> Configure the server port (default: 5100)");
40
53
  console.log(" --no-json Skip updating opencode.json");
41
54
  console.log(" --yes, -y Overwrite without prompting");
55
+ console.log(" --version, -v Show version number");
42
56
  };
43
57
 
44
58
  const installClaudeCodeHooks = () => {
@@ -129,7 +143,33 @@ const copyRecursiveSync = (src, dest) => {
129
143
  }
130
144
  };
131
145
 
146
+ const stopServer = (port) => {
147
+ try {
148
+ const pidOutput = execSync(`lsof -t -i :${port} 2>/dev/null`).toString().trim();
149
+ if (pidOutput) {
150
+ const pids = pidOutput.split('\n').map(p => parseInt(p, 10)).filter(p => !isNaN(p) && p > 0);
151
+ for (const pid of pids) {
152
+ try {
153
+ process.kill(pid);
154
+ console.log(`✓ Stopped server (PID: ${pid})`);
155
+ } catch {
156
+ // Process may have already exited
157
+ }
158
+ }
159
+ return pids.length > 0;
160
+ }
161
+ } catch {
162
+ // lsof failed, no process found
163
+ }
164
+ return false;
165
+ };
166
+
132
167
  const run = async () => {
168
+ if (shouldVersion) {
169
+ console.log(`opencode-pixel-office v${getVersion()}`);
170
+ process.exit(0);
171
+ }
172
+
133
173
  if (shouldSwitch) {
134
174
  if (switchTarget !== "opencode" && switchTarget !== "claude-code") {
135
175
  console.error("Switch target must be 'opencode' or 'claude-code'.");
@@ -148,6 +188,14 @@ const run = async () => {
148
188
  }
149
189
 
150
190
  if (shouldUninstall) {
191
+ // Stop server first
192
+ const config = loadConfig();
193
+ const port = config.port || 5100;
194
+ console.log("Stopping Pixel Office server...");
195
+ if (!stopServer(port)) {
196
+ console.log("- Server not running");
197
+ }
198
+
151
199
  const targetPluginPath = path.join(DEFAULT_PLUGIN_DIR, PLUGIN_NAME);
152
200
 
153
201
  // Remove Plugin
@@ -166,6 +214,56 @@ const run = async () => {
166
214
  console.log(`- App directory not found at ${DEFAULT_APP_DIR}`);
167
215
  }
168
216
 
217
+ // Remove Claude Code hooks
218
+ const claudeDir = path.join(os.homedir(), ".claude");
219
+ const hookDir = path.join(claudeDir, "hooks");
220
+ const hookFile = path.join(hookDir, "opencode-pixel-office-hook.js");
221
+ const settingsPath = path.join(claudeDir, "settings.json");
222
+
223
+ if (fs.existsSync(hookFile)) {
224
+ fs.unlinkSync(hookFile);
225
+ console.log(`✓ Removed Claude Code hook: ${hookFile}`);
226
+ }
227
+
228
+ if (fs.existsSync(settingsPath)) {
229
+ try {
230
+ const settings = JSON.parse(fs.readFileSync(settingsPath, "utf8"));
231
+ if (settings.hooks) {
232
+ let modified = false;
233
+ for (const eventName of Object.keys(settings.hooks)) {
234
+ const hooks = settings.hooks[eventName];
235
+ if (Array.isArray(hooks)) {
236
+ const filtered = hooks.filter(h => {
237
+ if (Array.isArray(h.hooks)) {
238
+ h.hooks = h.hooks.filter(inner =>
239
+ !inner.command?.includes("opencode-pixel-office-hook")
240
+ );
241
+ return h.hooks.length > 0;
242
+ }
243
+ return !h.command?.includes("opencode-pixel-office-hook");
244
+ });
245
+ if (filtered.length !== hooks.length) {
246
+ settings.hooks[eventName] = filtered;
247
+ modified = true;
248
+ }
249
+ if (filtered.length === 0) {
250
+ delete settings.hooks[eventName];
251
+ }
252
+ }
253
+ }
254
+ if (Object.keys(settings.hooks).length === 0) {
255
+ delete settings.hooks;
256
+ }
257
+ if (modified) {
258
+ fs.writeFileSync(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
259
+ console.log(`✓ Removed hooks from Claude Code settings`);
260
+ }
261
+ }
262
+ } catch {
263
+ // ignore settings parse errors
264
+ }
265
+ }
266
+
169
267
  // Clean up Config (Legacy)
170
268
  if (fs.existsSync(DEFAULT_CONFIG_PATH) && !skipJson) {
171
269
  try {
@@ -188,32 +286,73 @@ const run = async () => {
188
286
  process.exit(0);
189
287
  }
190
288
 
191
- if (args.includes("stop")) {
289
+ if (shouldStart) {
192
290
  const config = loadConfig();
193
- const port = config.port || 5100;
291
+ const port = portArg ? parseInt(portArg, 10) : (config.port || 5100);
292
+ const serverScript = "server/index.ts";
293
+
294
+ const rootSource = path.resolve(__dirname, "..");
295
+ const localServerPath = path.join(rootSource, "server", "index.ts");
296
+ const globalServerPath = path.join(DEFAULT_APP_DIR, "server", "index.ts");
297
+
298
+ let serverCwd;
299
+ let useGlobal = false;
300
+ if (fs.existsSync(localServerPath)) {
301
+ serverCwd = rootSource;
302
+ } else if (fs.existsSync(globalServerPath)) {
303
+ serverCwd = DEFAULT_APP_DIR;
304
+ useGlobal = true;
305
+ } else {
306
+ console.error("Server not found. Run 'opencode-pixel-office install' first.");
307
+ process.exit(1);
308
+ }
194
309
 
195
- console.log(`Stopping Pixel Office server on port ${port}...`);
196
310
  try {
197
- const pidOutput = execSync(`lsof -t -i :${port}`).toString().trim();
311
+ const pidOutput = execSync(`lsof -t -i :${port} 2>/dev/null`).toString().trim();
198
312
  if (pidOutput) {
199
- const pid = parseInt(pidOutput, 10);
200
- if (!isNaN(pid) && pid > 0) {
201
- process.kill(pid);
202
- console.log(`✓ Server stopped (PID: ${pid})`);
203
- } else {
204
- console.log(`- No server found on port ${port} (PID: ${pidOutput})`);
205
- }
206
- } else {
207
- console.log(`- No server found on port ${port}`);
313
+ console.log(`Pixel Office is already running on port ${port} (PID: ${pidOutput})`);
314
+ const url = `http://localhost:${port}`;
315
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
316
+ try { execSync(`${openCmd} ${url}`); } catch { }
317
+ console.log(`Opened ${url}`);
318
+ process.exit(0);
208
319
  }
209
- } catch (e) {
210
- if (e.message.includes("Command failed")) {
211
- // likely lsof failed meaning no process found
212
- console.log(`- No server found on port ${port}`);
213
- } else {
214
- console.error(`! Failed to stop server: ${e.message}`);
320
+ } catch { }
321
+
322
+ let tsxBin = "tsx";
323
+ if (useGlobal) {
324
+ const globalTsx = path.join(serverCwd, "node_modules", ".bin", "tsx");
325
+ if (fs.existsSync(globalTsx)) {
326
+ tsxBin = globalTsx;
215
327
  }
216
328
  }
329
+
330
+ const { spawn } = await import("node:child_process");
331
+ const child = spawn(tsxBin, [serverScript], {
332
+ cwd: serverCwd,
333
+ env: { ...process.env, PORT: String(port) },
334
+ detached: true,
335
+ stdio: "ignore",
336
+ });
337
+ child.unref();
338
+
339
+ console.log(`Pixel Office server started on port ${port} (PID: ${child.pid})`);
340
+
341
+ await new Promise((r) => setTimeout(r, 1500));
342
+ const url = `http://localhost:${port}`;
343
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
344
+ try { execSync(`${openCmd} ${url}`); } catch { }
345
+ console.log(`Opened ${url}`);
346
+ process.exit(0);
347
+ }
348
+
349
+ if (args.includes("stop")) {
350
+ const config = loadConfig();
351
+ const port = config.port || 5100;
352
+ console.log(`Stopping Pixel Office server on port ${port}...`);
353
+ if (!stopServer(port)) {
354
+ console.log(`- No server found on port ${port}`);
355
+ }
217
356
  process.exit(0);
218
357
  }
219
358
 
@@ -298,7 +437,8 @@ const run = async () => {
298
437
 
299
438
  console.log(`✓ Standalone app installed to ${DEFAULT_APP_DIR}`);
300
439
 
301
- console.log("\nSuccess! Restart OpenCode to launch Pixel Office.");
440
+ console.log("\nInstallation complete!");
441
+ console.log("Run 'opencode-pixel-office start' to launch the server.");
302
442
  };
303
443
 
304
444
  run().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-pixel-office",
3
- "version": "1.0.8",
3
+ "version": "1.0.10",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "bin",
@@ -3,31 +3,25 @@ import fs from "node:fs";
3
3
 
4
4
  const DEFAULT_PORT = 5100;
5
5
 
6
- // Helper to get configured port
7
- const getConfiguredPort = (globalDistDir) => {
6
+ const homeDir = process.env.HOME || process.env.USERPROFILE || "/";
7
+ const globalDistDir = path.join(homeDir, ".opencode", "pixel-office");
8
+
9
+ const getConfiguredPort = () => {
8
10
  try {
9
11
  const configPath = path.join(globalDistDir, "config.json");
10
12
  if (fs.existsSync(configPath)) {
11
- const start = Date.now();
12
- // Read synchronously to ensure we have port for endpoint
13
- // This is okay as it's a tiny local JSON
14
13
  const raw = fs.readFileSync(configPath, "utf8");
15
14
  const data = JSON.parse(raw);
16
15
  if (data && typeof data.port === "number") {
17
16
  return data.port;
18
17
  }
19
18
  }
20
- } catch (e) {
19
+ } catch {
21
20
  // ignore
22
21
  }
23
22
  return DEFAULT_PORT;
24
23
  };
25
24
 
26
- // We need to resolve home dir early to get config
27
- const homeDir = process.env.HOME || process.env.USERPROFILE || "/";
28
- const globalDistDir = path.join(homeDir, ".opencode", "pixel-office");
29
- const configuredPort = getConfiguredPort(globalDistDir);
30
-
31
25
  const readMode = () => {
32
26
  try {
33
27
  const configPath = path.join(globalDistDir, "config.json");
@@ -38,140 +32,28 @@ const readMode = () => {
38
32
  return data.mode;
39
33
  }
40
34
  }
41
- } catch (e) {
35
+ } catch {
42
36
  return "opencode";
43
37
  }
44
38
  return "opencode";
45
39
  };
46
40
 
47
41
  const resolveEndpoint = () => {
48
- const port = process.env.PIXEL_OFFICE_PORT || configuredPort;
49
- const raw =
50
- process.env.PIXEL_OFFICE_URL ||
51
- `http://localhost:${port}/events`;
42
+ const port = process.env.PIXEL_OFFICE_PORT || getConfiguredPort();
43
+ const raw = process.env.PIXEL_OFFICE_URL || `http://localhost:${port}/events`;
52
44
  return raw.endsWith("/events") ? raw : `${raw.replace(/\/$/, "")}/events`;
53
45
  };
54
46
 
55
- const resolveServerUrl = (endpoint) => endpoint.replace(/\/events$/, "");
56
-
57
- const resolveEndpointInfo = (endpoint, portOverride) => {
58
- try {
59
- const url = new URL(endpoint);
60
- const port = url.port ? Number(url.port) : portOverride;
61
- return {
62
- url,
63
- port,
64
- isLocal: url.hostname === "localhost" || url.hostname === "127.0.0.1",
65
- };
66
- } catch (error) {
67
- return { url: null, port: portOverride, isLocal: false };
68
- }
69
- };
70
-
71
- const resolveOpenCommand = (url) => {
72
- if (process.platform === "darwin") {
73
- return ["open", url];
74
- }
75
- if (process.platform === "win32") {
76
- return ["cmd", "/c", "start", "", url];
77
- }
78
- return ["xdg-open", url];
79
- };
80
-
81
47
  export const PixelOfficePlugin = async ({ directory, worktree, client }) => {
82
- const endpoint = resolveEndpoint();
83
- const serverUrl = resolveServerUrl(endpoint);
84
- const rootDir = worktree || directory;
85
- const endpointInfo = resolveEndpointInfo(endpoint, configuredPort);
86
- const openCommand = resolveOpenCommand(serverUrl);
87
-
88
- // Path Resolution
89
- const localServerPath = path.join(rootDir, "server", "index.ts");
90
- const globalServerPath = path.join(globalDistDir, "server", "index.ts");
91
-
92
- let serverCwd = rootDir;
93
- let serverScript = "server/index.ts";
94
- let useGlobal = false;
95
-
96
- if (fs.existsSync(localServerPath)) {
97
- // Local dev mode: run from workspace
98
- serverCwd = rootDir;
99
- } else if (fs.existsSync(globalServerPath)) {
100
- // Global standalone mode: run from ~/.opencode/pixel-office
101
- serverCwd = globalDistDir;
102
- useGlobal = true;
103
- } else {
104
- // Neither found
105
- if (endpointInfo.isLocal) {
106
- console.warn("[Pixel Office] Server not found locally or globally.");
107
- }
48
+ if (readMode() !== "opencode") {
49
+ // In claude-code mode, plugin does nothing - hooks handle events
50
+ return {};
108
51
  }
109
52
 
110
- let serverProcess = null;
111
- let browserOpened = false;
112
- let warnedOnce = false;
113
-
114
- const startServerIfNeeded = async () => {
115
- if (!endpointInfo.isLocal || serverProcess) {
116
- return;
117
- }
118
- const env = {
119
- ...process.env,
120
- PORT: String(process.env.PIXEL_OFFICE_PORT || endpointInfo.port),
121
- };
122
-
123
- let cmd = ["tsx", serverScript];
124
- if (useGlobal) {
125
- const globalTsx = path.join(serverCwd, "node_modules", ".bin", "tsx");
126
- if (fs.existsSync(globalTsx)) {
127
- cmd = [globalTsx, serverScript];
128
- }
129
- }
130
-
131
- try {
132
- serverProcess = Bun.spawn({
133
- cmd,
134
- cwd: serverCwd,
135
- env,
136
- stdout: "ignore",
137
- stderr: "ignore",
138
- });
139
- // console.log(`[Pixel Office] Started server in ${serverCwd} on port ${env.PORT}`);
140
- } catch (error) {
141
- await client.app.log({
142
- service: "pixel-office",
143
- level: "error",
144
- message: "Failed to start Pixel Office server",
145
- extra: { error: String(error), cwd: serverCwd },
146
- });
147
- }
148
- };
149
-
150
- const openBrowserIfNeeded = async () => {
151
- if (!endpointInfo.isLocal || browserOpened) {
152
- return;
153
- }
154
- browserOpened = true;
155
- try {
156
- Bun.spawn({ cmd: openCommand, stdout: "ignore", stderr: "ignore" });
157
- } catch (error) {
158
- await client.app.log({
159
- service: "pixel-office",
160
- level: "warn",
161
- message: "Failed to open Pixel Office browser",
162
- extra: { error: String(error), url: serverUrl },
163
- });
164
- }
165
- };
166
-
167
- await startServerIfNeeded();
168
- await openBrowserIfNeeded();
53
+ const endpoint = resolveEndpoint();
169
54
 
170
55
  return {
171
56
  event: async ({ event }) => {
172
- if (readMode() !== "opencode") {
173
- return;
174
- }
175
57
  if (!event) {
176
58
  return;
177
59
  }
@@ -183,18 +65,10 @@ export const PixelOfficePlugin = async ({ directory, worktree, client }) => {
183
65
  },
184
66
  body: JSON.stringify(event),
185
67
  });
186
- } catch (error) {
187
- if (!warnedOnce) {
188
- warnedOnce = true;
189
- }
190
- }
191
- },
192
- dispose: async () => {
193
- if (serverProcess) {
194
- // console.log("[Pixel Office] Killing server process...");
195
- serverProcess.kill();
196
- serverProcess = null;
68
+ } catch {
69
+ // Server not running, silently ignore
197
70
  }
198
71
  },
72
+ dispose: async () => {},
199
73
  };
200
74
  };