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
76 changes: 76 additions & 0 deletions .github/workflows/bump-core.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Bump @openconduit/core

# Triggered automatically when core publishes a new version, or manually
# via workflow_dispatch for out-of-band bumps.
on:
repository_dispatch:
types: [core-published]
workflow_dispatch:
inputs:
version:
description: 'New @openconduit/core version (e.g. 2.0.0-alpha.8)'
required: true
type: string

permissions:
contents: write
pull-requests: write
packages: read

jobs:
bump:
name: Update @openconduit/core and open PR
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
with:
ref: dev

- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
registry-url: 'https://npm.pkg.github.com'
scope: '@openconduit'

- name: Install current dependencies
run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Resolve target version
id: ver
run: |
if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
echo "version=${{ github.event.client_payload.version }}" >> $GITHUB_OUTPUT
else
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
fi

- name: Install new @openconduit/core
run: npm install @openconduit/core@${{ steps.ver.outputs.version }} -w packages/desktop
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Open Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: chore/bump-core-${{ steps.ver.outputs.version }}
base: dev
commit-message: "chore: bump @openconduit/core to ${{ steps.ver.outputs.version }}"
title: "chore: bump @openconduit/core to ${{ steps.ver.outputs.version }}"
body: |
Automated dependency bump triggered by a new `@openconduit/core` release.

| | |
|---|---|
| **New version** | `${{ steps.ver.outputs.version }}` |
| **File changed** | `packages/desktop/package.json` |

### After merging
Check `packages/desktop/src/shared/types.ts` for local type stubs whose
comment says they were declared to work around a missing package version.
If those types are now included in `@openconduit/core@${{ steps.ver.outputs.version }}`,
remove the local stubs to avoid duplicate declarations.
9 changes: 7 additions & 2 deletions packages/desktop/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { app, BrowserWindow, session, autoUpdater } from 'electron';
import { app, BrowserWindow, session, autoUpdater, crashReporter } from 'electron';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import started from 'electron-squirrel-startup';
Expand All @@ -9,6 +9,11 @@ import { destroyBrowserWindow } from './main/webtools/browser';

if (started) app.quit();

// Enable Crashpad crash reporter so minidumps are written to disk for renderer,
// GPU-process and main-process crashes. Must be called before the first window
// is created so that renderer processes inherit the reporter configuration.
crashReporter.start({ submitURL: '', uploadToServer: false });

// Pin userData to a stable name so it never moves when productName changes.
app.setPath('userData', path.join(app.getPath('appData'), 'openconduit'));

Expand Down Expand Up @@ -68,7 +73,7 @@ const createWindow = () => {

const err = new Error(`Renderer process gone (${details.reason}, exit ${details.exitCode})`);
err.name = 'RendererCrash';
void fireTelemetryCrash(err);
void fireTelemetryCrash(err, { crashDumpsDir: app.getPath('crashDumps') });

setTimeout(() => {
if (mainWindow.isDestroyed()) return;
Expand Down
47 changes: 46 additions & 1 deletion packages/desktop/src/main/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export async function fireTelemetrySessionStart(): Promise<void> {
});
}

export async function fireTelemetryCrash(error: Error): Promise<void> {
export async function fireTelemetryCrash(error: Error, extra?: { crashDumpsDir?: string }): Promise<void> {
// Always persist the crash locally so users can manually send it later
const sanitize = (s: string) =>
s.replace(/\(\/[^\s)]+\)/g, '(<path>)').replace(/at \/[^\s]+/g, 'at <path>').slice(0, 3000);
Expand All @@ -91,6 +91,7 @@ export async function fireTelemetryCrash(error: Error): Promise<void> {
errorMessage: error.message.replace(/(?:\/[\w.-]+){2,}/g, '<path>').slice(0, 300),
stackTrace: sanitize(error.stack ?? ''),
timestamp: new Date().toISOString(),
...(extra?.crashDumpsDir ? { crashDumpsDir: extra.crashDumpsDir } : {}),
});

if (!app.isPackaged) return; // never auto-send telemetry in dev
Expand Down Expand Up @@ -357,6 +358,50 @@ export function registerIpcHandlers(): void {
await shell.openPath(settingsStore.path);
});

// ─── Config Bundle Export / Import ──────────────────────────────────────────────
// Bundle = providers (no apiKey) + MCP servers (no headers/env). Safe to share.
ipcMain.handle('config:export-bundle', async (
_e,
meta: { name?: string; description?: string },
): Promise<boolean> => {
const win = BrowserWindow.getFocusedWindow();
const { canceled, filePath: dest } = await dialog.showSaveDialog(win!, {
title: 'Export Config Bundle',
defaultPath: 'openconduit-bundle.ocbundle',
filters: [
{ name: 'OpenConduit Bundle', extensions: ['ocbundle'] },
{ name: 'JSON', extensions: ['json'] },
],
});
if (canceled || !dest) return false;
const s = getSettings();
const bundle = {
version: 1,
name: meta?.name || undefined,
description: meta?.description || undefined,
providers: s.providers.map(({ apiKey: _k, ...rest }) => rest),
mcpServers: s.mcpServers.map(({ headers: _h, env: _e, ...rest }) => rest),
};
await fs.writeFile(dest, JSON.stringify(bundle, null, 2), 'utf-8');
return true;
});

ipcMain.handle('config:import-bundle', async (): Promise<import('../shared/types').ConfigBundle | null> => {
const win = BrowserWindow.getFocusedWindow();
const { canceled, filePaths } = await dialog.showOpenDialog(win!, {
title: 'Import Config Bundle',
filters: [{ name: 'OpenConduit Bundle', extensions: ['ocbundle', 'json'] }],
properties: ['openFile'],
});
if (canceled || filePaths.length === 0) return null;
const raw = await fs.readFile(filePaths[0], 'utf-8');
const parsed = JSON.parse(raw) as Record<string, unknown>;
if (!Array.isArray(parsed['providers']) || !Array.isArray(parsed['mcpServers'])) {
throw new Error('Invalid bundle: missing providers or mcpServers arrays');
}
return parsed as unknown as import('../shared/types').ConfigBundle;
});

// ─── Routing Evaluation ────────────────────────────────────────────────────
ipcMain.handle(
IPC.ROUTING_EVALUATE,
Expand Down
2 changes: 2 additions & 0 deletions packages/desktop/src/main/store/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ export interface StoredCrash {
errorMessage: string;
stackTrace: string;
timestamp: string;
/** Path to the Crashpad minidumps directory for this crash. */
crashDumpsDir?: string;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down
6 changes: 6 additions & 0 deletions packages/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ contextBridge.exposeInMainWorld('api', {
ipcRenderer.invoke(IPC.SETTINGS_IMPORT),
openSettingsFile: (): Promise<void> =>
ipcRenderer.invoke('settings:open-file'),
/** Export providers + MCP servers (no secrets) to a shareable .ocbundle file. */
exportBundle: (meta: { name?: string; description?: string }): Promise<boolean> =>
ipcRenderer.invoke('config:export-bundle', meta),
/** Open a .ocbundle file and return its contents for merging into settings. */
importBundle: (): Promise<import('./shared/types').ConfigBundle | null> =>
ipcRenderer.invoke('config:import-bundle'),
},
routing: {
evaluate: (params: {
Expand Down
4 changes: 4 additions & 0 deletions packages/desktop/src/renderer/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ declare global {
exportSettings: (redact: boolean) => Promise<boolean>;
importSettings: () => Promise<AppSettings | null>;
openSettingsFile: () => Promise<void>;
/** Export providers + MCP servers (no secrets) to a shareable .ocbundle file. */
exportBundle: (meta: { name?: string; description?: string }) => Promise<boolean>;
/** Open a .ocbundle file and return its contents for merging into settings. */
importBundle: () => Promise<import('../shared/types').ConfigBundle | null>;
};
routing: {
evaluate: (params: {
Expand Down
18 changes: 13 additions & 5 deletions packages/desktop/src/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@
// This stub re-exports everything so main-process code keeps working unchanged.
export * from '@openconduit/core/types';

// ── Local augmentation ────────────────────────────────────────────────────────
// `manifest?` was added to InstalledExtensionInfo in the core repo (Phase 5).
// Re-declare the type here until a new @openconduit/core version is published
// and the dependency is bumped.
import type { InstalledExtensionInfo as _Base } from '@openconduit/core/types';
// ── Local augmentations ───────────────────────────────────────────────────────
// Types added to core but not yet published. Declared here until
// @openconduit/core is bumped to include them.
import type { ProviderConfig, McpServerConfig, InstalledExtensionInfo as _Base } from '@openconduit/core/types';
export interface ConfigBundle {
version: 1;
name?: string;
description?: string;
providers: Omit<ProviderConfig, 'apiKey'>[];
mcpServers: Omit<McpServerConfig, 'headers' | 'env'>[];
}

// ── InstalledExtensionInfo (manifest field) ───────────────────────────────────
export type InstalledExtensionInfo = _Base & {
/**
* Pre-read contents of the extension's `manifest.json`.
Expand Down
Loading