Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,29 @@ OrgX plugin peer for **OpenCode**. One of three reference peers (alongside `orgx

**The peer model:** this plugin opens its own authenticated WebSocket to OrgX server, receives `task.dispatch` messages, runs them in your local OpenCode session (your subscription pays the tokens), and posts receipts + deviations back. It also writes compact, redacted Work Graph events locally so audit-first reconciliation can preserve progress and fingerprints across signup. No central broker. If another peer goes down, this one keeps running.

## Install + run
## Install

OpenCode can load the peer as a native plugin from `opencode.json`:

```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["@useorgx/orgx-opencode-plugin"]
}
```

Then start OpenCode with the OrgX credentials available in the environment:

```bash
export ORGX_API_KEY=oxk_...
export ORGX_WORKSPACE_ID=<uuid>
opencode
```

The native plugin starts the OrgX peer when the local OpenCode server connects.
Set `ORGX_BASE_URL` only when testing against a non-production OrgX API.

You can also run the peer directly:

```bash
npm install -g @useorgx/orgx-opencode-plugin
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@
"@useorgx/orgx-gateway-sdk": "git+https://github.com/useorgx/orgx-gateway-sdk.git#main"
},
"devDependencies": {
"@opencode-ai/plugin": "^1.15.7",
"@types/node": "^22.0.0",
"typescript": "^5.4.0",
"vitest": "^2.0.0"
},
"keywords": ["orgx", "opencode", "plugin-peer", "byok"],
"keywords": ["orgx", "opencode", "opencode-plugin", "plugin-peer", "byok"],
"repository": {
"type": "git",
"url": "git+https://github.com/useorgx/orgx-opencode-plugin.git"
Expand Down
2 changes: 1 addition & 1 deletion src/OpenCodeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import type {
PeerToServerMessage,
} from '@useorgx/orgx-gateway-sdk';

import { recordWorkGraphEvent } from './workGraphOutbox';
import { recordWorkGraphEvent } from './workGraphOutbox.js';

type OpenCodeState = {
port: number;
Expand Down
19 changes: 16 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,20 @@
export { OpenCodeDriver, type OpenCodeDriverOptions } from './OpenCodeDriver';
export { startPeer } from './peer';
export { OpenCodeDriver, type OpenCodeDriverOptions } from './OpenCodeDriver.js';
export {
default,
createOrgXOpenCodePlugin,
OrgXOpenCodePlugin,
type CreateOrgXOpenCodePluginOptions,
} from './plugin.js';
export type { StartedPeer, StartPeerOptions } from './peer.js';
export {
buildWorkGraphEventRecord,
recordWorkGraphEvent,
resolveWorkGraphOutboxPath,
} from './workGraphOutbox';
} from './workGraphOutbox.js';

export async function startPeer(
opts: import('./peer.js').StartPeerOptions
): Promise<import('./peer.js').StartedPeer> {
const peer = await import('./peer.js');
return peer.startPeer(opts);
}
2 changes: 1 addition & 1 deletion src/peer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { readFile } from 'fs/promises';
import { resolve } from 'path';
import { fileURLToPath } from 'url';

import { OpenCodeDriver } from './OpenCodeDriver';
import { OpenCodeDriver } from './OpenCodeDriver.js';

export type StartPeerOptions = {
apiKey: string;
Expand Down
122 changes: 122 additions & 0 deletions src/plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it, vi } from 'vitest';

import { createOrgXOpenCodePlugin } from './plugin';

type PluginHooks = {
event: (input: { event: { type?: string } }) => Promise<void>;
};

function createLogger() {
return {
log: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
};
}

async function loadHooks(
opts: Parameters<typeof createOrgXOpenCodePlugin>[0]
): Promise<PluginHooks> {
const plugin = createOrgXOpenCodePlugin(opts);
return (await plugin({} as never)) as PluginHooks;
}

describe('OrgXOpenCodePlugin', () => {
it('starts the peer on server.connected with env config', async () => {
const stop = vi.fn();
const startPeer = vi.fn(async () => ({ stop }));
const logger = createLogger();
const hooks = await loadHooks({
startPeer,
logger,
env: {
ORGX_API_KEY: 'oxk_test',
ORGX_WORKSPACE_ID: 'workspace-123',
ORGX_BASE_URL: 'https://example.org',
},
});

await hooks.event({ event: { type: 'session.created' } });
expect(startPeer).not.toHaveBeenCalled();

await hooks.event({ event: { type: 'server.connected' } });
expect(startPeer).toHaveBeenCalledTimes(1);
expect(startPeer).toHaveBeenCalledWith({
apiKey: 'oxk_test',
workspaceId: 'workspace-123',
baseUrl: 'https://example.org',
});
expect(logger.log).toHaveBeenCalledWith(
'[orgx-opencode-plugin] native OpenCode plugin peer started'
);
});

it('does not start more than once', async () => {
const startPeer = vi.fn(async () => ({ stop: vi.fn() }));
const logger = createLogger();
const hooks = await loadHooks({
startPeer,
logger,
env: {
ORGX_API_KEY: 'oxk_test',
ORGX_WORKSPACE_ID: 'workspace-123',
},
});

await hooks.event({ event: { type: 'server.connected' } });
await hooks.event({ event: { type: 'server.connected' } });

expect(startPeer).toHaveBeenCalledTimes(1);
});

it('warns once when required env config is missing', async () => {
const startPeer = vi.fn(async () => ({ stop: vi.fn() }));
const logger = createLogger();
const hooks = await loadHooks({
startPeer,
logger,
env: {},
});

await hooks.event({ event: { type: 'server.connected' } });
await hooks.event({ event: { type: 'server.connected' } });

expect(startPeer).not.toHaveBeenCalled();
expect(logger.warn).toHaveBeenCalledTimes(1);
expect(logger.warn).toHaveBeenCalledWith(
'[orgx-opencode-plugin] native plugin loaded, but ORGX_API_KEY and ORGX_WORKSPACE_ID are required to connect'
);
});

it('logs start failures without throwing through OpenCode hooks', async () => {
const startPeer = vi.fn(async () => {
throw new Error('connect failed');
});
const logger = createLogger();
const hooks = await loadHooks({
startPeer,
logger,
env: {
ORGX_API_KEY: 'oxk_test',
ORGX_WORKSPACE_ID: 'workspace-123',
},
});

await expect(
hooks.event({ event: { type: 'server.connected' } })
).resolves.toBeUndefined();

expect(logger.error).toHaveBeenCalledWith(
'[orgx-opencode-plugin] failed to start peer',
'connect failed'
);
});

it('loads the package entry without eagerly importing the peer runtime', async () => {
const mod = await import('./index');

expect(typeof mod.default).toBe('function');
expect(typeof mod.OrgXOpenCodePlugin).toBe('function');
expect(typeof mod.startPeer).toBe('function');
});
});
71 changes: 71 additions & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Plugin } from '@opencode-ai/plugin';

import type { StartedPeer, StartPeerOptions } from './peer.js';

type StartPeer = (opts: StartPeerOptions) => Promise<StartedPeer>;
type Env = Record<string, string | undefined>;
type Logger = Pick<Console, 'log' | 'warn' | 'error'>;

export type CreateOrgXOpenCodePluginOptions = {
startPeer?: StartPeer;
env?: Env;
logger?: Logger;
};

export function createOrgXOpenCodePlugin(
opts: CreateOrgXOpenCodePluginOptions = {}
): Plugin {
const start = opts.startPeer ?? defaultStartPeer;
const env = opts.env ?? process.env;
const logger = opts.logger ?? console;
let peer: Promise<StartedPeer> | null = null;
let warnedMissingConfig = false;

async function startIfConfigured() {
if (peer) return;

const apiKey = env.ORGX_API_KEY;
const workspaceId = env.ORGX_WORKSPACE_ID;
const baseUrl = env.ORGX_BASE_URL;

if (!apiKey || !workspaceId) {
if (!warnedMissingConfig) {
logger.warn(
'[orgx-opencode-plugin] native plugin loaded, but ORGX_API_KEY and ORGX_WORKSPACE_ID are required to connect'
);
warnedMissingConfig = true;
}
return;
}

peer = start({ apiKey, workspaceId, baseUrl });
try {
await peer;
logger.log('[orgx-opencode-plugin] native OpenCode plugin peer started');
} catch (err) {
peer = null;
logger.error('[orgx-opencode-plugin] failed to start peer', formatError(err));
}
}

return async () => ({
event: async ({ event }: { event: { type?: string } }) => {
if (event.type === 'server.connected') {
await startIfConfigured();
}
},
});
}

async function defaultStartPeer(opts: StartPeerOptions): Promise<StartedPeer> {
const { startPeer } = await import('./peer.js');
return startPeer(opts);
}

function formatError(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}

export const OrgXOpenCodePlugin = createOrgXOpenCodePlugin();
export default OrgXOpenCodePlugin;
Loading