parallax-opencode 0.2.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.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * PARALLAX Discord Rich Presence Module
3
+ *
4
+ * ## BROKEN
5
+ * Discord RPC is not working. The @xhayper/discord-rpc library connects
6
+ * via IPC but presence never appears. Suspected issues:
7
+ * - esbuild bundling may mangle the IPC transport (node:net, node:fs, node:path)
8
+ * - Discord IPC pipe discovery may fail in OpenCode's process context
9
+ * - The "ready" event sequence from login() may not match expectations
10
+ * Needs proper debugging with a test harness outside the plugin bundle.
11
+ *
12
+ * Transferred from the parallax Rust codebase (src/rpc/mod.rs).
13
+ * Uses the same Discord Application ID (1498076324357476494) and
14
+ * the "parallaxtui" large image asset already registered in the
15
+ * Discord Developer Portal for that application.
16
+ *
17
+ * Image switching by agent:
18
+ * - Agent "parallax" -> largeImageKey = "parallaxtui"
19
+ * - Agent "opencode" -> largeImageKey = "opencode.png"
20
+ * - Agent null -> no image, text only
21
+ * - details always -> "OpenCode"
22
+ *
23
+ * Integrates with the existing plugin hook model rather than
24
+ * registering standalone hooks, to avoid conflicts with the
25
+ * Parallax protocol enforcement hooks.
26
+ *
27
+ * References:
28
+ * - Original Rust implementation: parallax/src/rpc/mod.rs
29
+ * - Reference TS plugin: phoenixak/opencode-discord-rpc
30
+ */
31
+ export type RpcStatus = "coding" | "idle" | "thinking" | "waiting";
32
+ /**
33
+ * Which agent type is currently active.
34
+ * - "parallax": Parallax engine is in control; show parallaxtui image
35
+ * - "opencode": OpenCode built-in modes; show opencode.png
36
+ * - null: Unknown / none; text only, no image
37
+ */
38
+ export type RpcAgent = "parallax" | "opencode" | null;
39
+ export interface RpcPresenceData {
40
+ status: RpcStatus;
41
+ modelName?: string;
42
+ mode?: string;
43
+ agent?: RpcAgent;
44
+ }
45
+ export declare class DiscordRpcManager {
46
+ private client;
47
+ private isConnected;
48
+ private connecting;
49
+ private retryCount;
50
+ private retryTimer;
51
+ private sessionStart;
52
+ private currentPresence;
53
+ private destroyed;
54
+ private maxRetries;
55
+ private retryIntervalMs;
56
+ connect(): Promise<boolean>;
57
+ private scheduleReconnect;
58
+ startSession(): void;
59
+ clearSession(): void;
60
+ updatePresence(data: RpcPresenceData): Promise<void>;
61
+ private setActivity;
62
+ clearPresence(): Promise<void>;
63
+ destroy(): Promise<void>;
64
+ private statusLabel;
65
+ private log;
66
+ get connected(): boolean;
67
+ }
68
+ /**
69
+ * Map an OpenCode agent selector name to an RPC agent type.
70
+ *
71
+ * OpenCode agents you TAB between:
72
+ * "Parallax" -> "parallax" (show parallaxtui.png)
73
+ * "Plan", "Build", "Agent", "Debug" (OpenCode built-in) -> "opencode" (opencode.png)
74
+ * anything else / null -> null (text only, no image)
75
+ */
76
+ export declare function resolveAgent(agentName: string | null | undefined): RpcAgent;
77
+ export declare function getDiscordRpc(): DiscordRpcManager;
78
+ export declare function initDiscordRpc(): Promise<DiscordRpcManager>;
79
+ export declare function destroyDiscordRpc(): Promise<void>;
@@ -0,0 +1,297 @@
1
+ /**
2
+ * PARALLAX Discord Rich Presence Module
3
+ *
4
+ * ## BROKEN
5
+ * Discord RPC is not working. The @xhayper/discord-rpc library connects
6
+ * via IPC but presence never appears. Suspected issues:
7
+ * - esbuild bundling may mangle the IPC transport (node:net, node:fs, node:path)
8
+ * - Discord IPC pipe discovery may fail in OpenCode's process context
9
+ * - The "ready" event sequence from login() may not match expectations
10
+ * Needs proper debugging with a test harness outside the plugin bundle.
11
+ *
12
+ * Transferred from the parallax Rust codebase (src/rpc/mod.rs).
13
+ * Uses the same Discord Application ID (1498076324357476494) and
14
+ * the "parallaxtui" large image asset already registered in the
15
+ * Discord Developer Portal for that application.
16
+ *
17
+ * Image switching by agent:
18
+ * - Agent "parallax" -> largeImageKey = "parallaxtui"
19
+ * - Agent "opencode" -> largeImageKey = "opencode.png"
20
+ * - Agent null -> no image, text only
21
+ * - details always -> "OpenCode"
22
+ *
23
+ * Integrates with the existing plugin hook model rather than
24
+ * registering standalone hooks, to avoid conflicts with the
25
+ * Parallax protocol enforcement hooks.
26
+ *
27
+ * References:
28
+ * - Original Rust implementation: parallax/src/rpc/mod.rs
29
+ * - Reference TS plugin: phoenixak/opencode-discord-rpc
30
+ */
31
+ import { Client } from "@xhayper/discord-rpc";
32
+ // ---------------------------------------------------------------------------
33
+ // Constants -- transferred from parallax Rust RPC module
34
+ // ---------------------------------------------------------------------------
35
+ const PARALLAX_CLIENT_ID = "1498076324357476494";
36
+ // Parallax images (uploaded to Discord Developer Portal for this app)
37
+ // Discord strips file extensions -- key is just the name.
38
+ const PARALLAX_IMAGE_KEY = "parallaxtui";
39
+ const PARALLAX_IMAGE_TEXT = "ParallaxTUI";
40
+ // OpenCode fallback image
41
+ const OPENCODE_IMAGE_KEY = "opencode";
42
+ const OPENCODE_IMAGE_TEXT = "OpenCode";
43
+ // ---------------------------------------------------------------------------
44
+ // RPC Manager
45
+ // ---------------------------------------------------------------------------
46
+ export class DiscordRpcManager {
47
+ client = null;
48
+ isConnected = false;
49
+ connecting = false;
50
+ retryCount = 0;
51
+ retryTimer = null;
52
+ sessionStart = null;
53
+ currentPresence = null;
54
+ destroyed = false;
55
+ maxRetries = 10;
56
+ retryIntervalMs = 15000;
57
+ // -----------------------------------------------------------------------
58
+ // Connection
59
+ // -----------------------------------------------------------------------
60
+ async connect() {
61
+ if (this.destroyed)
62
+ return false;
63
+ if (this.isConnected || this.connecting)
64
+ return this.isConnected;
65
+ this.connecting = true;
66
+ try {
67
+ this.client = new Client({ clientId: PARALLAX_CLIENT_ID });
68
+ this.client.on("ready", () => {
69
+ this.isConnected = true;
70
+ this.connecting = false;
71
+ this.retryCount = 0;
72
+ this.log("Connected to Discord Rich Presence");
73
+ // Restore presence if we had one before reconnect
74
+ if (this.currentPresence) {
75
+ this.setActivity(this.currentPresence);
76
+ }
77
+ });
78
+ this.client.on("disconnected", () => {
79
+ this.isConnected = false;
80
+ this.log("Disconnected from Discord", "warn");
81
+ if (!this.destroyed)
82
+ this.scheduleReconnect();
83
+ });
84
+ // login() calls connect() internally then emits "ready".
85
+ // Using just connect() would never fire the "ready" event.
86
+ await this.client.login();
87
+ return true;
88
+ }
89
+ catch (err) {
90
+ this.connecting = false;
91
+ const msg = err instanceof Error ? err.message : String(err);
92
+ if (msg.includes("ENOENT") || msg.includes("Could not connect")) {
93
+ this.log("Discord not running or not accessible", "debug");
94
+ }
95
+ else {
96
+ this.log(`Discord RPC connection failed: ${msg}`, "warn");
97
+ }
98
+ if (!this.destroyed)
99
+ this.scheduleReconnect();
100
+ return false;
101
+ }
102
+ }
103
+ scheduleReconnect() {
104
+ if (this.destroyed)
105
+ return;
106
+ if (this.retryCount >= this.maxRetries) {
107
+ this.log(`Discord RPC: max retries (${this.maxRetries}) reached, giving up`, "warn");
108
+ return;
109
+ }
110
+ this.retryCount++;
111
+ if (this.retryTimer)
112
+ clearTimeout(this.retryTimer);
113
+ this.retryTimer = setTimeout(() => {
114
+ if (!this.destroyed)
115
+ this.connect();
116
+ }, this.retryIntervalMs);
117
+ }
118
+ // -----------------------------------------------------------------------
119
+ // Session lifecycle
120
+ // -----------------------------------------------------------------------
121
+ startSession() {
122
+ this.sessionStart = new Date();
123
+ this.log("RPC session started, timer reset", "debug");
124
+ }
125
+ clearSession() {
126
+ this.sessionStart = null;
127
+ }
128
+ // -----------------------------------------------------------------------
129
+ // Presence
130
+ // -----------------------------------------------------------------------
131
+ async updatePresence(data) {
132
+ this.currentPresence = data;
133
+ if (!this.isConnected || !this.client?.user)
134
+ return;
135
+ await this.setActivity(data);
136
+ }
137
+ async setActivity(data) {
138
+ if (!this.client?.user)
139
+ return;
140
+ // details is always "OpenCode" regardless of agent
141
+ const details = "OpenCode";
142
+ // state shows status + optional mode suffix
143
+ const statusText = this.statusLabel(data.status);
144
+ const state = data.mode
145
+ ? `${statusText} | ${data.mode}`
146
+ : statusText;
147
+ const activity = {
148
+ details,
149
+ state,
150
+ };
151
+ // Choose the image based on agent type
152
+ if (data.agent === "parallax") {
153
+ activity.largeImageKey = PARALLAX_IMAGE_KEY;
154
+ activity.largeImageText = PARALLAX_IMAGE_TEXT;
155
+ activity.smallImageKey = PARALLAX_IMAGE_KEY;
156
+ activity.smallImageText = `${PARALLAX_IMAGE_TEXT} | ${statusText}`;
157
+ }
158
+ else if (data.agent === "opencode") {
159
+ activity.largeImageKey = OPENCODE_IMAGE_KEY;
160
+ activity.largeImageText = OPENCODE_IMAGE_TEXT;
161
+ activity.smallImageKey = OPENCODE_IMAGE_KEY;
162
+ activity.smallImageText = `${OPENCODE_IMAGE_TEXT} | ${statusText}`;
163
+ }
164
+ // agent === null: no images, text only
165
+ if (this.sessionStart) {
166
+ activity.startTimestamp = this.sessionStart;
167
+ }
168
+ if (data.mode) {
169
+ activity.buttons = [
170
+ {
171
+ label: `Mode: ${data.mode}`,
172
+ url: "https://github.com/Master0fFate/parallax-opencode",
173
+ },
174
+ ];
175
+ }
176
+ try {
177
+ await this.client.user.setActivity(activity);
178
+ }
179
+ catch (err) {
180
+ const msg = err instanceof Error ? err.message : String(err);
181
+ this.log(`Failed to set activity: ${msg}`, "error");
182
+ }
183
+ }
184
+ async clearPresence() {
185
+ if (!this.isConnected || !this.client?.user)
186
+ return;
187
+ try {
188
+ await this.client.user.clearActivity();
189
+ this.currentPresence = null;
190
+ }
191
+ catch (err) {
192
+ const msg = err instanceof Error ? err.message : String(err);
193
+ this.log(`Failed to clear activity: ${msg}`, "error");
194
+ }
195
+ }
196
+ // -----------------------------------------------------------------------
197
+ // Teardown
198
+ // -----------------------------------------------------------------------
199
+ async destroy() {
200
+ this.destroyed = true;
201
+ if (this.retryTimer) {
202
+ clearTimeout(this.retryTimer);
203
+ this.retryTimer = null;
204
+ }
205
+ if (this.client) {
206
+ try {
207
+ await this.clearPresence();
208
+ this.client.destroy();
209
+ }
210
+ catch {
211
+ // Ignore cleanup errors
212
+ }
213
+ this.client = null;
214
+ }
215
+ this.isConnected = false;
216
+ this.connecting = false;
217
+ this.log("Discord RPC destroyed", "info");
218
+ }
219
+ // -----------------------------------------------------------------------
220
+ // Internal
221
+ // -----------------------------------------------------------------------
222
+ statusLabel(status) {
223
+ switch (status) {
224
+ case "coding":
225
+ return "Coding...";
226
+ case "idle":
227
+ return "Idle";
228
+ case "thinking":
229
+ return "Thinking...";
230
+ case "waiting":
231
+ return "Waiting for input...";
232
+ default:
233
+ return "OpenCode";
234
+ }
235
+ }
236
+ log(msg, level = "info") {
237
+ const prefix = "[parallax:discord-rpc]";
238
+ switch (level) {
239
+ case "warn":
240
+ console.warn(`${prefix} ${msg}`);
241
+ break;
242
+ case "error":
243
+ console.error(`${prefix} ${msg}`);
244
+ break;
245
+ case "debug":
246
+ if (process.env.DEBUG)
247
+ console.debug(`${prefix} ${msg}`);
248
+ break;
249
+ default:
250
+ console.log(`${prefix} ${msg}`);
251
+ }
252
+ }
253
+ get connected() {
254
+ return this.isConnected;
255
+ }
256
+ }
257
+ // ---------------------------------------------------------------------------
258
+ // Helper: resolve agent type from OpenCode agent name
259
+ // ---------------------------------------------------------------------------
260
+ /**
261
+ * Map an OpenCode agent selector name to an RPC agent type.
262
+ *
263
+ * OpenCode agents you TAB between:
264
+ * "Parallax" -> "parallax" (show parallaxtui.png)
265
+ * "Plan", "Build", "Agent", "Debug" (OpenCode built-in) -> "opencode" (opencode.png)
266
+ * anything else / null -> null (text only, no image)
267
+ */
268
+ export function resolveAgent(agentName) {
269
+ if (!agentName)
270
+ return null;
271
+ const lower = agentName.toLowerCase();
272
+ if (lower === "parallax")
273
+ return "parallax";
274
+ // Everything else is an OpenCode built-in agent
275
+ return "opencode";
276
+ }
277
+ // ---------------------------------------------------------------------------
278
+ // Singleton
279
+ // ---------------------------------------------------------------------------
280
+ let _instance = null;
281
+ export function getDiscordRpc() {
282
+ if (!_instance) {
283
+ _instance = new DiscordRpcManager();
284
+ }
285
+ return _instance;
286
+ }
287
+ export async function initDiscordRpc() {
288
+ const mgr = getDiscordRpc();
289
+ await mgr.connect();
290
+ return mgr;
291
+ }
292
+ export async function destroyDiscordRpc() {
293
+ if (_instance) {
294
+ await _instance.destroy();
295
+ _instance = null;
296
+ }
297
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * PARALLAX ENGINE -- Canonical TypeScript Plugin
3
+ *
4
+ * Consolidated source of truth for the Parallax Engine OpenCode plugin.
5
+ * Contains all 7 custom tools, mode state machine (free/plan/build/debug),
6
+ * protocol enforcement, friction-loop verification, skill injection,
7
+ * session state preservation, and trace recording.
8
+ *
9
+ * License: MIT
10
+ * Copyright (c) 2026 Master0fFate
11
+ */
12
+ declare const _default: {
13
+ id: string;
14
+ server: ({ client }: import("@opencode-ai/plugin").PluginInput) => Promise<{
15
+ tool: {
16
+ parallax_verify: {
17
+ description: string;
18
+ args: {};
19
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
20
+ };
21
+ parallax_analyze: {
22
+ description: string;
23
+ args: {
24
+ topic: import("zod").ZodString;
25
+ };
26
+ execute(args: {
27
+ topic: string;
28
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
29
+ };
30
+ parallax_checkin: {
31
+ description: string;
32
+ args: {
33
+ step: import("zod").ZodString;
34
+ };
35
+ execute(args: {
36
+ step: string;
37
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
38
+ };
39
+ parallax_plan: {
40
+ description: string;
41
+ args: {};
42
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
43
+ };
44
+ parallax_build: {
45
+ description: string;
46
+ args: {};
47
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
48
+ };
49
+ parallax_debug: {
50
+ description: string;
51
+ args: {};
52
+ execute(args: Record<string, never>, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
53
+ };
54
+ parallax_trace_export: {
55
+ description: string;
56
+ args: {
57
+ pretty: import("zod").ZodOptional<import("zod").ZodBoolean>;
58
+ };
59
+ execute(args: {
60
+ pretty?: boolean | undefined;
61
+ }, context: import("@opencode-ai/plugin").ToolContext): Promise<import("@opencode-ai/plugin").ToolResult>;
62
+ };
63
+ };
64
+ "tool.execute.before": (input: {
65
+ tool: string;
66
+ }) => Promise<void>;
67
+ "tool.execute.after": (input: {
68
+ tool: string;
69
+ args?: Record<string, unknown>;
70
+ }) => Promise<void>;
71
+ event: (input: {
72
+ event: {
73
+ type: string;
74
+ properties?: Record<string, unknown>;
75
+ };
76
+ }) => Promise<void>;
77
+ "chat.message": (input: {
78
+ sessionID: string;
79
+ agent?: string;
80
+ model?: {
81
+ modelID?: string;
82
+ };
83
+ }) => Promise<void>;
84
+ "chat.params": (input: {
85
+ sessionID: string;
86
+ agent: string;
87
+ model: {
88
+ id: string;
89
+ };
90
+ }) => Promise<void>;
91
+ "experimental.chat.system.transform": (_input: unknown, output: {
92
+ system?: string[];
93
+ }) => Promise<void>;
94
+ "experimental.session.compacting": (_input: unknown, output: {
95
+ context?: string[];
96
+ }) => Promise<void>;
97
+ }>;
98
+ };
99
+ export default _default;