diff --git a/.github/workflows/bump-core.yml b/.github/workflows/bump-core.yml new file mode 100644 index 0000000..6968843 --- /dev/null +++ b/.github/workflows/bump-core.yml @@ -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. diff --git a/packages/desktop/src/main.ts b/packages/desktop/src/main.ts index 39d2e51..7c55acd 100644 --- a/packages/desktop/src/main.ts +++ b/packages/desktop/src/main.ts @@ -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'; @@ -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')); @@ -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; diff --git a/packages/desktop/src/main/ipc.ts b/packages/desktop/src/main/ipc.ts index 805bdba..7766733 100644 --- a/packages/desktop/src/main/ipc.ts +++ b/packages/desktop/src/main/ipc.ts @@ -79,7 +79,7 @@ export async function fireTelemetrySessionStart(): Promise { }); } -export async function fireTelemetryCrash(error: Error): Promise { +export async function fireTelemetryCrash(error: Error, extra?: { crashDumpsDir?: string }): Promise { // Always persist the crash locally so users can manually send it later const sanitize = (s: string) => s.replace(/\(\/[^\s)]+\)/g, '()').replace(/at \/[^\s]+/g, 'at ').slice(0, 3000); @@ -91,6 +91,7 @@ export async function fireTelemetryCrash(error: Error): Promise { errorMessage: error.message.replace(/(?:\/[\w.-]+){2,}/g, '').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 @@ -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 => { + 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 => { + 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; + 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, diff --git a/packages/desktop/src/main/store/settings.ts b/packages/desktop/src/main/store/settings.ts index 752cb2d..23a4167 100644 --- a/packages/desktop/src/main/store/settings.ts +++ b/packages/desktop/src/main/store/settings.ts @@ -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 diff --git a/packages/desktop/src/preload.ts b/packages/desktop/src/preload.ts index 8d85ab9..3cfcb1b 100644 --- a/packages/desktop/src/preload.ts +++ b/packages/desktop/src/preload.ts @@ -121,6 +121,12 @@ contextBridge.exposeInMainWorld('api', { ipcRenderer.invoke(IPC.SETTINGS_IMPORT), openSettingsFile: (): Promise => ipcRenderer.invoke('settings:open-file'), + /** Export providers + MCP servers (no secrets) to a shareable .ocbundle file. */ + exportBundle: (meta: { name?: string; description?: string }): Promise => + ipcRenderer.invoke('config:export-bundle', meta), + /** Open a .ocbundle file and return its contents for merging into settings. */ + importBundle: (): Promise => + ipcRenderer.invoke('config:import-bundle'), }, routing: { evaluate: (params: { diff --git a/packages/desktop/src/renderer/env.d.ts b/packages/desktop/src/renderer/env.d.ts index b31fe89..8d13244 100644 --- a/packages/desktop/src/renderer/env.d.ts +++ b/packages/desktop/src/renderer/env.d.ts @@ -60,6 +60,10 @@ declare global { exportSettings: (redact: boolean) => Promise; importSettings: () => Promise; openSettingsFile: () => Promise; + /** Export providers + MCP servers (no secrets) to a shareable .ocbundle file. */ + exportBundle: (meta: { name?: string; description?: string }) => Promise; + /** Open a .ocbundle file and return its contents for merging into settings. */ + importBundle: () => Promise; }; routing: { evaluate: (params: { diff --git a/packages/desktop/src/shared/types.ts b/packages/desktop/src/shared/types.ts index 7e1c60c..3a1227c 100644 --- a/packages/desktop/src/shared/types.ts +++ b/packages/desktop/src/shared/types.ts @@ -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[]; + mcpServers: Omit[]; +} + +// ── InstalledExtensionInfo (manifest field) ─────────────────────────────────── export type InstalledExtensionInfo = _Base & { /** * Pre-read contents of the extension's `manifest.json`.