Skip to content
Closed
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
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,27 @@ npx tsx examples/research/competitor-analysis.ts \

---

## Demo App

An interactive Electron desktop application showcasing all SDK capabilities:

- API key connection and agent model selection
- Session create/list/delete
- Real-time SSE event streaming via IPC
- Live screenshot viewer (auto-refresh every 3s)
- Pause/Resume/Cancel controls
- Quick action cards for common tasks

```bash
cd demo && npm install
cd .. && npm install
AGI_API_KEY=your-key npx electron --require ts-node/register demo/main.ts
```

See [`demo/`](./demo) for the full source.

---

## Documentation & Resources

**Learn More**
Expand Down
194 changes: 194 additions & 0 deletions demo/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
/**
* AGI Node.js SDK Demo - Electron Main Process
*
* Creates AGIClient, exposes operations via IPC handlers,
* and forwards SSE events to the renderer.
*/

import { app, BrowserWindow, ipcMain } from 'electron';
import * as path from 'path';
import { AGIClient } from '../src';
import type {
SessionResponse,
SSEEvent,
ScreenshotResponse,
} from '../src/types';

let mainWindow: BrowserWindow | null = null;
let client: AGIClient | null = null;
let activeStreamAbort: AbortController | null = null;
let screenshotInterval: ReturnType<typeof setInterval> | null = null;

function createWindow() {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
title: 'AGI SDK Demo',
backgroundColor: '#1a1a2e',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});

mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html'));
mainWindow.on('closed', () => {
mainWindow = null;
});
}

app.whenReady().then(createWindow);

app.on('window-all-closed', () => {
stopScreenshots();
stopStream();
if (client) {
client = null;
}
app.quit();
});

// --- IPC Handlers ---

ipcMain.handle('connect', async (_event, apiKey: string) => {
try {
client = new AGIClient({ apiKey });
return { success: true };
} catch (err: any) {
return { success: false, error: err.message };
}
});

ipcMain.handle('list-models', async () => {
if (!client) return { models: [] };
try {
const result = await client.sessions.listModels('cdp');
return result;
} catch {
return { models: [] };
}
});

ipcMain.handle('create-session', async (_event, agentName: string) => {
if (!client) throw new Error('Not connected');
const session = await client.sessions.create(agentName);
return toPlain(session);
});

ipcMain.handle('list-sessions', async () => {
if (!client) return [];
const sessions = await client.sessions.list();
return sessions.map(toPlain);
});

ipcMain.handle('delete-session', async (_event, sessionId: string) => {
if (!client) throw new Error('Not connected');
return await client.sessions.delete(sessionId);
});

ipcMain.handle(
'send-message',
async (_event, sessionId: string, message: string) => {
if (!client) throw new Error('Not connected');
return await client.sessions.sendMessage(sessionId, message);
}
);

ipcMain.handle('pause-session', async (_event, sessionId: string) => {
if (!client) throw new Error('Not connected');
return await client.sessions.pause(sessionId);
});

ipcMain.handle('resume-session', async (_event, sessionId: string) => {
if (!client) throw new Error('Not connected');
return await client.sessions.resume(sessionId);
});

ipcMain.handle('cancel-session', async (_event, sessionId: string) => {
if (!client) throw new Error('Not connected');
return await client.sessions.cancel(sessionId);
});

ipcMain.handle('screenshot', async (_event, sessionId: string) => {
if (!client) throw new Error('Not connected');
const resp = await client.sessions.screenshot(sessionId);
return toPlain(resp);
});

// --- SSE Streaming ---

ipcMain.on('start-stream', (_event, sessionId: string) => {
stopStream();
startStream(sessionId);
});

ipcMain.on('stop-stream', () => {
stopStream();
});

async function startStream(sessionId: string) {
if (!client || !mainWindow) return;

activeStreamAbort = new AbortController();

try {
for await (const event of client.sessions.streamEvents(sessionId, {
includeHistory: true,
})) {
if (activeStreamAbort.signal.aborted) break;
mainWindow?.webContents.send('sse-event', toPlain(event));
if (event.event === 'done' || event.event === 'error') break;
}
} catch (err: any) {
if (!activeStreamAbort.signal.aborted) {
mainWindow?.webContents.send('sse-error', err.message);
}
}

mainWindow?.webContents.send('sse-ended');
}

function stopStream() {
if (activeStreamAbort) {
activeStreamAbort.abort();
activeStreamAbort = null;
}
}

// --- Screenshot polling ---

ipcMain.on('start-screenshots', (_event, sessionId: string) => {
stopScreenshots();
startScreenshots(sessionId);
});

ipcMain.on('stop-screenshots', () => {
stopScreenshots();
});

function startScreenshots(sessionId: string) {
if (!client || !mainWindow) return;

screenshotInterval = setInterval(async () => {
try {
if (!client || !mainWindow) return;
const resp = await client.sessions.screenshot(sessionId);
mainWindow?.webContents.send('screenshot-update', toPlain(resp));
} catch {
// Silently ignore screenshot errors
}
}, 3000);
}

function stopScreenshots() {
if (screenshotInterval) {
clearInterval(screenshotInterval);
screenshotInterval = null;
}
}

// Strip class instances to plain objects for IPC serialization
function toPlain<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
17 changes: 17 additions & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "agi-sdk-demo",
"version": "1.0.0",
"description": "AGI Node.js SDK Demo - Electron GUI",
"main": "main.ts",
"scripts": {
"start": "electron --require ts-node/register main.ts",
"build": "tsc"
},
"dependencies": {
"electron": "^33.0.0"
},
"devDependencies": {
"ts-node": "^10.9.2",
"typescript": "^5.6.0"
}
}
59 changes: 59 additions & 0 deletions demo/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Preload script - exposes typed API to renderer via contextBridge.
*/

import { contextBridge, ipcRenderer } from 'electron';

export interface AgiAPI {
connect(apiKey: string): Promise<{ success: boolean; error?: string }>;
listModels(): Promise<{ models: any[] }>;
createSession(agentName: string): Promise<any>;
listSessions(): Promise<any[]>;
deleteSession(sessionId: string): Promise<any>;
sendMessage(sessionId: string, message: string): Promise<any>;
pauseSession(sessionId: string): Promise<any>;
resumeSession(sessionId: string): Promise<any>;
cancelSession(sessionId: string): Promise<any>;
screenshot(sessionId: string): Promise<any>;
startStream(sessionId: string): void;
stopStream(): void;
startScreenshots(sessionId: string): void;
stopScreenshots(): void;
onSSEEvent(callback: (event: any) => void): void;
onSSEError(callback: (error: string) => void): void;
onSSEEnded(callback: () => void): void;
onScreenshotUpdate(callback: (data: any) => void): void;
}

const api: AgiAPI = {
connect: (apiKey) => ipcRenderer.invoke('connect', apiKey),
listModels: () => ipcRenderer.invoke('list-models'),
createSession: (agentName) =>
ipcRenderer.invoke('create-session', agentName),
listSessions: () => ipcRenderer.invoke('list-sessions'),
deleteSession: (sessionId) =>
ipcRenderer.invoke('delete-session', sessionId),
sendMessage: (sessionId, message) =>
ipcRenderer.invoke('send-message', sessionId, message),
pauseSession: (sessionId) =>
ipcRenderer.invoke('pause-session', sessionId),
resumeSession: (sessionId) =>
ipcRenderer.invoke('resume-session', sessionId),
cancelSession: (sessionId) =>
ipcRenderer.invoke('cancel-session', sessionId),
screenshot: (sessionId) => ipcRenderer.invoke('screenshot', sessionId),
startStream: (sessionId) => ipcRenderer.send('start-stream', sessionId),
stopStream: () => ipcRenderer.send('stop-stream'),
startScreenshots: (sessionId) =>
ipcRenderer.send('start-screenshots', sessionId),
stopScreenshots: () => ipcRenderer.send('stop-screenshots'),
onSSEEvent: (callback) =>
ipcRenderer.on('sse-event', (_e, event) => callback(event)),
onSSEError: (callback) =>
ipcRenderer.on('sse-error', (_e, error) => callback(error)),
onSSEEnded: (callback) => ipcRenderer.on('sse-ended', () => callback()),
onScreenshotUpdate: (callback) =>
ipcRenderer.on('screenshot-update', (_e, data) => callback(data)),
Comment on lines +50 to +56
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused _e parameter in event listeners.

Suggested change
onSSEEvent: (callback) =>
ipcRenderer.on('sse-event', (_e, event) => callback(event)),
onSSEError: (callback) =>
ipcRenderer.on('sse-error', (_e, error) => callback(error)),
onSSEEnded: (callback) => ipcRenderer.on('sse-ended', () => callback()),
onScreenshotUpdate: (callback) =>
ipcRenderer.on('screenshot-update', (_e, data) => callback(data)),
onSSEEvent: (callback) =>
ipcRenderer.on('sse-event', (_, event) => callback(event)),
onSSEError: (callback) =>
ipcRenderer.on('sse-error', (_, error) => callback(error)),
onSSEEnded: (callback) => ipcRenderer.on('sse-ended', () => callback()),
onScreenshotUpdate: (callback) =>
ipcRenderer.on('screenshot-update', (_, data) => callback(data)),

Context Used: Rule from dashboard - Remove unused parameters from function signatures to keep them clean and focused on only the require... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: demo/preload.ts
Line: 50:56

Comment:
Unused `_e` parameter in event listeners.

```suggestion
  onSSEEvent: (callback) =>
    ipcRenderer.on('sse-event', (_, event) => callback(event)),
  onSSEError: (callback) =>
    ipcRenderer.on('sse-error', (_, error) => callback(error)),
  onSSEEnded: (callback) => ipcRenderer.on('sse-ended', () => callback()),
  onScreenshotUpdate: (callback) =>
    ipcRenderer.on('screenshot-update', (_, data) => callback(data)),
```

**Context Used:** Rule from `dashboard` - Remove unused parameters from function signatures to keep them clean and focused on only the require... ([source](https://app.greptile.com/review/custom-context?memory=ddab3f1f-d2e3-4c54-a6e5-702d1aabe12c))

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

};

contextBridge.exposeInMainWorld('agi', api);
Loading
Loading