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/bin/aegis.js +30 -0
- package/build/icon.png +0 -0
- package/lib/local/agents.js +102 -0
- package/lib/local/context.js +81 -0
- package/lib/local/engine.js +460 -0
- package/lib/local/ollama.js +77 -0
- package/lib/local/prompt.js +91 -0
- package/lib/local/providers.js +536 -0
- package/lib/local/shell.js +208 -0
- package/lib/local/tools.js +638 -0
- package/lib/settings.js +225 -0
- package/lib/sync/memory-queue.js +57 -0
- package/lib/sync/sessions.js +199 -0
- package/main.js +715 -0
- package/package.json +46 -0
- package/preload.js +168 -0
- package/renderer/app.js +1990 -0
- package/renderer/index.html +289 -0
- package/renderer/max-tokens.js +18 -0
- package/renderer/style.css +1454 -0
- package/vendor/aegis.js +694 -0
- package/vendor/foreign-memory.js +666 -0
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));
|