zerogterm 0.9.0-alpha.1 → 0.9.1-alpha.1
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.
- package/README.md +54 -1
- package/dist/main/main/main.js +55 -103
- package/dist/renderer/assets/{index-DYB0NFWF.js → index-CknqVqkF.js} +22 -22
- package/dist/renderer/assets/index-axEdZFb8.css +1 -0
- package/dist/renderer/index.html +2 -2
- package/package.json +1 -1
- package/dist/renderer/assets/index-BovKjPi0.css +0 -1
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ See the project walkthrough on [YouTube](https://youtu.be/4aJZCxLHD14).
|
|
|
38
38
|
|
|
39
39
|
## Release status
|
|
40
40
|
|
|
41
|
-
ZeroG Terminal is currently a public alpha. The current release is `0.9.
|
|
41
|
+
ZeroG Terminal is currently a public alpha. The current release is `0.9.1-alpha.1`; the version history is tracked in [versions.txt](versions.txt). The running version is shown beside the wordmark in the title bar, read from the app itself rather than written into the interface, so it is accurate in a packaged build too.
|
|
42
42
|
|
|
43
43
|
The GitHub Releases page provides a Windows x64 installer and portable executable for each desktop release. These alpha builds are intended for early adopters and testing rather than production use. The npm package remains available for developers who prefer to launch ZeroG from Node.js.
|
|
44
44
|
|
|
@@ -396,6 +396,59 @@ npm install --global zerogterm
|
|
|
396
396
|
zerogterm
|
|
397
397
|
```
|
|
398
398
|
|
|
399
|
+
## Connecting an AI agent through MCP
|
|
400
|
+
|
|
401
|
+
ZeroG Terminal can expose the running desktop instance as a local MCP server. This
|
|
402
|
+
lets an MCP-capable AI agent inspect workspaces and sessions, restore or create
|
|
403
|
+
project workspaces, open SSH sessions, and request commands for your approval.
|
|
404
|
+
|
|
405
|
+
1. Start ZeroG Terminal with MCP enabled:
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
ZEROG_MCP_ENABLED=1 npx zerogterm
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
On Windows PowerShell, use:
|
|
412
|
+
|
|
413
|
+
```powershell
|
|
414
|
+
$env:ZEROG_MCP_ENABLED = "1"
|
|
415
|
+
npx zerogterm
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
2. Open **Settings → AI & voice → MCP** in ZeroG Terminal. Copy the displayed
|
|
419
|
+
endpoint and bearer token into your MCP client. The default endpoint is
|
|
420
|
+
`http://127.0.0.1:60056/mcp`.
|
|
421
|
+
|
|
422
|
+
MCP clients that support Streamable HTTP generally need the equivalent of:
|
|
423
|
+
|
|
424
|
+
```json
|
|
425
|
+
{
|
|
426
|
+
"url": "http://127.0.0.1:60056/mcp",
|
|
427
|
+
"headers": {
|
|
428
|
+
"Authorization": "Bearer <token-from-zero-g-settings>"
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
The exact configuration key differs between clients; use its Streamable HTTP
|
|
434
|
+
or remote MCP server setting rather than a stdio command. The token belongs to
|
|
435
|
+
the current ZeroG process, is held only in memory, and must be copied again
|
|
436
|
+
after ZeroG restarts. Never put it in source control, prompts, issue reports,
|
|
437
|
+
or shared configuration.
|
|
438
|
+
|
|
439
|
+
3. Ask the agent to call `zerog_get_connection_status`, then
|
|
440
|
+
`zerog_acquire_control` with a client ID and the capabilities it needs. Read
|
|
441
|
+
capabilities require no write access; workspace changes and command requests
|
|
442
|
+
require the corresponding lease capabilities. The lease is single-client and
|
|
443
|
+
expires unless renewed.
|
|
444
|
+
|
|
445
|
+
Every command request is shown in ZeroG Terminal for exact user approval before
|
|
446
|
+
anything is written to a shell. Rejection, cancellation, takeover, credential
|
|
447
|
+
prompts, uncertain prompts, and unsupported command shapes fail closed. MCP does
|
|
448
|
+
not receive terminal output, credentials, or keystroke-level control. See the
|
|
449
|
+
[full MCP protocol and security guide](docs/mcp.md) for tools, limits, and
|
|
450
|
+
troubleshooting.
|
|
451
|
+
|
|
399
452
|
## Development
|
|
400
453
|
|
|
401
454
|
```bash
|
package/dist/main/main/main.js
CHANGED
|
@@ -381,61 +381,64 @@ ipcMain.handle('mcp:execution:cancel', (_event, requestId) => {
|
|
|
381
381
|
win?.webContents.send('mcp:execution', result);
|
|
382
382
|
return result;
|
|
383
383
|
});
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
clearMcpExecution(requestId, session.id);
|
|
402
|
-
win?.webContents.send('mcp:execution', result);
|
|
403
|
-
}
|
|
384
|
+
function createMcpProviders() {
|
|
385
|
+
return {
|
|
386
|
+
listWorkspaces: () => workspaceStore.load(),
|
|
387
|
+
listSessions: () => service.list(),
|
|
388
|
+
createWorkspace,
|
|
389
|
+
restoreWorkspace,
|
|
390
|
+
createSshSession: async (request) => {
|
|
391
|
+
const session = await service.createSsh(request.target, request.name);
|
|
392
|
+
const attached = service.attach(session.id, (data) => {
|
|
393
|
+
const requestId = mcpActiveExecutions.get(session.id);
|
|
394
|
+
if (requestId) {
|
|
395
|
+
const output = `${mcpExecutionOutput.get(requestId) ?? ''}${data}`.slice(0, 16 * 1024);
|
|
396
|
+
mcpExecutionOutput.set(requestId, output);
|
|
397
|
+
if ((output.match(/ExitCode=\d+/g) ?? []).length >= 2) {
|
|
398
|
+
const result = mcpExecutions.finishRunning(requestId, 'completed', output, false, 'Command completed.');
|
|
399
|
+
clearMcpExecution(requestId, session.id);
|
|
400
|
+
win?.webContents.send('mcp:execution', result);
|
|
404
401
|
}
|
|
405
|
-
win?.webContents.send('terminal:data', session.id, data);
|
|
406
|
-
}, (message) => win?.webContents.send('terminal:status', session.id, message));
|
|
407
|
-
win?.webContents.send('mcp:ssh-session-created', { session: attached });
|
|
408
|
-
return attached;
|
|
409
|
-
},
|
|
410
|
-
createProjectWorkspace,
|
|
411
|
-
requestCommand: async (request) => {
|
|
412
|
-
const sessions = await service.list();
|
|
413
|
-
const session = sessions.find((item) => item.id === request.sessionId);
|
|
414
|
-
if (!session)
|
|
415
|
-
throw new Error('Unknown session.');
|
|
416
|
-
const result = mcpExecutions.create({ clientId: request.clientId, sessionId: request.sessionId, sessionKind: session.kind, command: request.command, policy: { allowRemoteSessions: session.kind === 'ssh', ...(request.timeoutMs === undefined ? {} : { maxRuntimeMs: request.timeoutMs }), ...(request.outputBytes === undefined ? {} : { maxOutputBytes: request.outputBytes }) } });
|
|
417
|
-
const safeRemote = session.kind === 'ssh' && isSafeRemoteCommand(result.command);
|
|
418
|
-
if (safeRemote) {
|
|
419
|
-
const approved = mcpExecutions.approveAutomatically(result.requestId);
|
|
420
|
-
mcpAudit.record({ at: Date.now(), requestId: approved.requestId, clientId: approved.clientId, capability: 'session:execute', sessionId: approved.sessionId, state: 'approved', command: approved.displayCommand });
|
|
421
|
-
await runMcpExecution(approved.requestId);
|
|
422
|
-
return mcpExecutions.get(approved.requestId, request.clientId);
|
|
423
402
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
403
|
+
win?.webContents.send('terminal:data', session.id, data);
|
|
404
|
+
}, (message) => win?.webContents.send('terminal:status', session.id, message));
|
|
405
|
+
win?.webContents.send('mcp:ssh-session-created', { session: attached });
|
|
406
|
+
return attached;
|
|
407
|
+
},
|
|
408
|
+
createProjectWorkspace,
|
|
409
|
+
requestCommand: async (request) => {
|
|
410
|
+
const sessions = await service.list();
|
|
411
|
+
const session = sessions.find((item) => item.id === request.sessionId);
|
|
412
|
+
if (!session)
|
|
413
|
+
throw new Error('Unknown session.');
|
|
414
|
+
const result = mcpExecutions.create({ clientId: request.clientId, sessionId: request.sessionId, sessionKind: session.kind, command: request.command, policy: { allowRemoteSessions: session.kind === 'ssh', ...(request.timeoutMs === undefined ? {} : { maxRuntimeMs: request.timeoutMs }), ...(request.outputBytes === undefined ? {} : { maxOutputBytes: request.outputBytes }) } });
|
|
415
|
+
const safeRemote = session.kind === 'ssh' && isSafeRemoteCommand(result.command);
|
|
416
|
+
if (safeRemote) {
|
|
417
|
+
const approved = mcpExecutions.approveAutomatically(result.requestId);
|
|
418
|
+
mcpAudit.record({ at: Date.now(), requestId: approved.requestId, clientId: approved.clientId, capability: 'session:execute', sessionId: approved.sessionId, state: 'approved', command: approved.displayCommand });
|
|
419
|
+
await runMcpExecution(approved.requestId);
|
|
420
|
+
return mcpExecutions.get(approved.requestId, request.clientId);
|
|
437
421
|
}
|
|
422
|
+
mcpAudit.record({ at: Date.now(), requestId: result.requestId, clientId: result.clientId, capability: 'session:execute', sessionId: result.sessionId, state: 'requested', command: result.displayCommand });
|
|
423
|
+
win?.webContents.send('mcp:execution', result);
|
|
424
|
+
return result;
|
|
438
425
|
},
|
|
426
|
+
getCommandResult: async (request) => mcpExecutions.get(request.requestId, request.clientId),
|
|
427
|
+
cancelCommand: async (request) => {
|
|
428
|
+
const current = mcpExecutions.get(request.requestId, request.clientId);
|
|
429
|
+
const result = mcpExecutions.cancel(request.requestId, request.clientId);
|
|
430
|
+
const session = 'sessionId' in current ? (await service.list()).find((item) => item.id === current.sessionId) : undefined;
|
|
431
|
+
if (session?.kind === 'local')
|
|
432
|
+
service.write(session.id, '\u0003');
|
|
433
|
+
clearMcpExecution(request.requestId);
|
|
434
|
+
return result;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
ipcMain.handle('mcp:start', async () => {
|
|
439
|
+
mcpHost ??= new McpServerHost({
|
|
440
|
+
control: mcpControl,
|
|
441
|
+
providers: createMcpProviders(),
|
|
439
442
|
version: app.getVersion()
|
|
440
443
|
});
|
|
441
444
|
return mcpHost.start();
|
|
@@ -707,58 +710,7 @@ app.whenReady().then(() => {
|
|
|
707
710
|
if (process.env.ZEROG_MCP_ENABLED === '1') {
|
|
708
711
|
mcpHost ??= new McpServerHost({
|
|
709
712
|
control: mcpControl,
|
|
710
|
-
providers:
|
|
711
|
-
listWorkspaces: () => workspaceStore.load(),
|
|
712
|
-
listSessions: () => service.list(),
|
|
713
|
-
createWorkspace,
|
|
714
|
-
restoreWorkspace,
|
|
715
|
-
createSshSession: async (request) => {
|
|
716
|
-
const session = await service.createSsh(request.target, request.name);
|
|
717
|
-
const attached = service.attach(session.id, (data) => {
|
|
718
|
-
const requestId = mcpActiveExecutions.get(session.id);
|
|
719
|
-
if (requestId) {
|
|
720
|
-
const output = `${mcpExecutionOutput.get(requestId) ?? ''}${data}`.slice(0, 16 * 1024);
|
|
721
|
-
mcpExecutionOutput.set(requestId, output);
|
|
722
|
-
if ((output.match(/ExitCode=\d+/g) ?? []).length >= 2) {
|
|
723
|
-
const result = mcpExecutions.finishRunning(requestId, 'completed', output, false, 'Command completed.');
|
|
724
|
-
clearMcpExecution(requestId, session.id);
|
|
725
|
-
win?.webContents.send('mcp:execution', result);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
win?.webContents.send('terminal:data', session.id, data);
|
|
729
|
-
}, (message) => win?.webContents.send('terminal:status', session.id, message));
|
|
730
|
-
win?.webContents.send('mcp:ssh-session-created', { session: attached });
|
|
731
|
-
return attached;
|
|
732
|
-
},
|
|
733
|
-
createProjectWorkspace,
|
|
734
|
-
requestCommand: async (request) => {
|
|
735
|
-
const sessions = await service.list();
|
|
736
|
-
const session = sessions.find((item) => item.id === request.sessionId);
|
|
737
|
-
if (!session)
|
|
738
|
-
throw new Error('Unknown session.');
|
|
739
|
-
const result = mcpExecutions.create({ clientId: request.clientId, sessionId: request.sessionId, sessionKind: session.kind, command: request.command, policy: { allowRemoteSessions: session.kind === 'ssh', ...(request.timeoutMs === undefined ? {} : { maxRuntimeMs: request.timeoutMs }), ...(request.outputBytes === undefined ? {} : { maxOutputBytes: request.outputBytes }) } });
|
|
740
|
-
const safeRemote = session.kind === 'ssh' && isSafeRemoteCommand(result.command);
|
|
741
|
-
if (safeRemote) {
|
|
742
|
-
const approved = mcpExecutions.approveAutomatically(result.requestId);
|
|
743
|
-
mcpAudit.record({ at: Date.now(), requestId: approved.requestId, clientId: approved.clientId, capability: 'session:execute', sessionId: approved.sessionId, state: 'approved', command: approved.displayCommand });
|
|
744
|
-
await runMcpExecution(approved.requestId);
|
|
745
|
-
return mcpExecutions.get(approved.requestId, request.clientId);
|
|
746
|
-
}
|
|
747
|
-
mcpAudit.record({ at: Date.now(), requestId: result.requestId, clientId: result.clientId, capability: 'session:execute', sessionId: result.sessionId, state: 'requested', command: result.displayCommand });
|
|
748
|
-
win?.webContents.send('mcp:execution', result);
|
|
749
|
-
return result;
|
|
750
|
-
},
|
|
751
|
-
getCommandResult: async (request) => mcpExecutions.get(request.requestId, request.clientId),
|
|
752
|
-
cancelCommand: async (request) => {
|
|
753
|
-
const current = mcpExecutions.get(request.requestId, request.clientId);
|
|
754
|
-
const result = mcpExecutions.cancel(request.requestId, request.clientId);
|
|
755
|
-
const session = 'sessionId' in current ? (await service.list()).find((item) => item.id === current.sessionId) : undefined;
|
|
756
|
-
if (session?.kind === 'local')
|
|
757
|
-
service.write(session.id, '\u0003');
|
|
758
|
-
clearMcpExecution(request.requestId);
|
|
759
|
-
return result;
|
|
760
|
-
}
|
|
761
|
-
},
|
|
713
|
+
providers: createMcpProviders(),
|
|
762
714
|
version: app.getVersion()
|
|
763
715
|
});
|
|
764
716
|
void mcpHost.start();
|