aegis-desktop 0.3.0

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/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "aegis-desktop",
3
+ "productName": "AEGIS Desktop",
4
+ "version": "0.3.0",
5
+ "description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
6
+ "author": {
7
+ "name": "AEGIS Code",
8
+ "email": "nborneklint@gmail.com"
9
+ },
10
+ "license": "MIT",
11
+ "main": "main.js",
12
+ "homepage": "https://aegiscloud.org",
13
+ "bin": {
14
+ "aegis": "bin/aegis.js"
15
+ },
16
+ "files": [
17
+ "main.js",
18
+ "preload.js",
19
+ "renderer",
20
+ "lib",
21
+ "vendor",
22
+ "bin",
23
+ "build"
24
+ ],
25
+ "scripts": {
26
+ "start": "electron .",
27
+ "predist": "node scripts/predist.mjs",
28
+ "prepublishOnly": "npm run predist",
29
+ "icon": "node scripts/generate-icon.mjs",
30
+ "dist": "npm run predist && electron-builder",
31
+ "dist:dir": "npm run predist && electron-builder --dir",
32
+ "check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/max-tokens.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check lib/local/context.js && node --check lib/local/providers.js && node --check lib/local/ollama.js && node --check lib/local/engine.js && node --check lib/local/tools.js && node --check lib/local/prompt.js && node --check lib/local/shell.js && node --check lib/local/agents.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check bin/aegis.js",
33
+ "test:shell": "node ../test/desktop-shell.mjs",
34
+ "test:model": "node ../test/model-dispatch.mjs",
35
+ "test:max-tokens": "node ../test/max-tokens.test.mjs",
36
+ "test:foreign-memory": "node ../test/foreign-memory.test.mjs",
37
+ "test:tools": "node ../test/local-tools.test.mjs",
38
+ "test:engine": "node ../test/local-engine.test.mjs"
39
+ },
40
+ "dependencies": {
41
+ "electron": "^33.0.0"
42
+ },
43
+ "devDependencies": {
44
+ "electron-builder": "^25.1.8"
45
+ }
46
+ }
package/preload.js ADDED
@@ -0,0 +1,168 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Preload — the ONLY bridge between the isolated renderer and the main
5
+ * process. Exposes a whitelisted window.aegis.* surface backed by
6
+ * ipcRenderer.invoke over the aegis:<method> channels registered in main.js.
7
+ *
8
+ * Security: contextIsolation + sandbox are enabled in main.js; no Node
9
+ * globals leak through this bridge. Every method returns the backend's plain
10
+ * JSON payload, and the API key never crosses this bridge — the renderer only
11
+ * ever sees a masked preview via `status`.
12
+ */
13
+
14
+ const { contextBridge, ipcRenderer } = require('electron');
15
+
16
+ const IPC_PREFIX = 'aegis:';
17
+ const MODEL_PREFIX = 'model:';
18
+ const SYNC_PREFIX = 'sync:';
19
+ const CHAT_DELTA_CHANNEL = `${IPC_PREFIX}chatDelta`;
20
+
21
+ function invoke(name, payload) {
22
+ return ipcRenderer.invoke(
23
+ IPC_PREFIX + name,
24
+ payload === undefined ? undefined : payload
25
+ );
26
+ }
27
+
28
+ function invokeModel(name, payload) {
29
+ return ipcRenderer.invoke(
30
+ MODEL_PREFIX + name,
31
+ payload === undefined ? undefined : payload
32
+ );
33
+ }
34
+
35
+ function invokeSync(name, payload) {
36
+ return ipcRenderer.invoke(
37
+ SYNC_PREFIX + name,
38
+ payload === undefined ? undefined : payload
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Build the chatDelta listener for one streaming request.
44
+ *
45
+ * Chunks arrive on a single shared channel, tagged by the main process with
46
+ * the sessionId of the request they came from (see `taggedChunk` in main.js).
47
+ * Filtering here is what makes concurrent streams safe: the primary answer and
48
+ * each card of the horizontal discovery lane subscribe with their own
49
+ * sessionId, so one reply can never bleed into another card's body.
50
+ *
51
+ * The normalisation is deliberately loose-strict: `undefined`/`''` ids
52
+ * (legacy single-stream callers that pass no sessionId) are treated as one
53
+ * bucket, so the old behaviour — receive every untagged chunk — is preserved
54
+ * byte for byte.
55
+ */
56
+ function deltaListener(sessionId, onDelta) {
57
+ const want = sessionId || null;
58
+ return (_event, chunk) => {
59
+ const id = chunk && chunk.id ? chunk.id : null;
60
+ if (id !== want) return;
61
+ onDelta(chunk);
62
+ };
63
+ }
64
+
65
+ // Whitelist — mirrors the dispatch map in main.js. If a method is not listed
66
+ // here the renderer cannot call it: add surface here AND there deliberately.
67
+ const api = {
68
+ status: () => invoke('status'),
69
+ // In-app key entry: the raw key flows renderer -> main only (never back).
70
+ // The main process persists it encrypted and returns a masked preview.
71
+ setApiKey: (key) => invoke('setApiKey', { key }),
72
+ verifyApiKey: () => invoke('verifyApiKey'),
73
+ tokenBankBalance: () => invoke('tokenBankBalance'),
74
+ listModels: () => invoke('listModels'),
75
+ // Streaming chat (D2.1): pass an onDelta callback to receive SSE chunks as
76
+ // they arrive (pushed from main over aegis:chatDelta). The invoke promise
77
+ // resolves once with the normalised final result. Without a callback the
78
+ // request is plain non-streaming, exactly as before.
79
+ chatCompletion: (payload, onDelta) => {
80
+ const opts = payload || {};
81
+ if (typeof onDelta !== 'function') {
82
+ return invoke('chatCompletion', { ...opts, stream: false });
83
+ }
84
+ const listener = deltaListener(opts.sessionId, onDelta);
85
+ const cleanup = () =>
86
+ ipcRenderer.removeListener(CHAT_DELTA_CHANNEL, listener);
87
+ ipcRenderer.on(CHAT_DELTA_CHANNEL, listener);
88
+ return invoke('chatCompletion', { ...opts, stream: true }).then(
89
+ (result) => {
90
+ cleanup();
91
+ return result;
92
+ },
93
+ (err) => {
94
+ cleanup();
95
+ throw err;
96
+ }
97
+ );
98
+ },
99
+ byokStatus: () => invoke('byokStatus'),
100
+ byokSet: (provider, apiKey) => invoke('byokSet', { provider, apiKey }),
101
+ memorySearch: (query, limit) => invoke('memorySearch', { query, limit }),
102
+ memorySave: (entry) => invoke('memorySave', { entry }),
103
+ memoryList: (limit) => invoke('memoryList', { limit }),
104
+ verifyToken: (token) => invoke('verifyToken', { token }),
105
+ memoryActivate: (token) => invoke('memoryActivate', { token }),
106
+ memoryPull: (since) => invoke('memoryPull', { since }),
107
+ memorySaveBatch: (entries) => invoke('memorySaveBatch', { entries }),
108
+ memoryImport: (payload) => invoke('memoryImport', payload || {}),
109
+ importConversation: (payload) => invoke('importConversation', payload || {}),
110
+ };
111
+
112
+ // Model-class surface (plan P1 §5.3): backed by the `model:` channels in
113
+ // main.js. Full keys never cross this bridge — settings.get/set return only
114
+ // masked previews.
115
+ const models = {
116
+ listClasses: () => invokeModel('listClasses'),
117
+ listModels: (cls) => invokeModel('listModels', { class: cls }),
118
+ // Always streaming: pass an onDelta callback to receive live chunks over
119
+ // CHAT_DELTA_CHANNEL; the invoke resolves once with the final result.
120
+ chat: (payload, onDelta) => {
121
+ const opts = payload || {};
122
+ if (typeof onDelta !== 'function') {
123
+ return invokeModel('chat', opts);
124
+ }
125
+ const listener = deltaListener(opts.sessionId, onDelta);
126
+ const cleanup = () =>
127
+ ipcRenderer.removeListener(CHAT_DELTA_CHANNEL, listener);
128
+ ipcRenderer.on(CHAT_DELTA_CHANNEL, listener);
129
+ return invokeModel('chat', opts).then(
130
+ (result) => {
131
+ cleanup();
132
+ return result;
133
+ },
134
+ (err) => {
135
+ cleanup();
136
+ throw err;
137
+ }
138
+ );
139
+ },
140
+ settings: {
141
+ get: () => invokeModel('settings.get'),
142
+ set: (provider, cfg) =>
143
+ invokeModel('settings.set', {
144
+ provider,
145
+ baseURL: cfg && cfg.baseURL,
146
+ key: cfg && cfg.key,
147
+ }),
148
+ remove: (provider) => invokeModel('settings.remove', { provider }),
149
+ },
150
+ cancel: (sessionId) => invokeModel('cancel', { sessionId }),
151
+ };
152
+
153
+ // Session sync surface (plan P1 §5.3 / P3 §7): local persistence now, cloud
154
+ // push/pull later.
155
+ const sync = {
156
+ listSessions: () => invokeSync('listSessions'),
157
+ open: (sessionId) => invokeSync('open', { sessionId }),
158
+ save: (session) => invokeSync('save', session || {}),
159
+ append: (sessionId, message) => invokeSync('append', { sessionId, message }),
160
+ delete: (sessionId) => invokeSync('delete', { sessionId }),
161
+ push: () => invokeSync('push'),
162
+ pull: () => invokeSync('pull'),
163
+ status: () => invokeSync('status'),
164
+ };
165
+
166
+ contextBridge.exposeInMainWorld('aegis', Object.freeze(api));
167
+ contextBridge.exposeInMainWorld('models', Object.freeze(models));
168
+ contextBridge.exposeInMainWorld('sync', Object.freeze(sync));