aegis-desktop 0.5.0 → 0.5.3
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/lib/local/engine.js +243 -29
- package/lib/local/prompt.js +9 -2
- package/lib/local/providers.js +69 -14
- package/lib/local/tools.js +24 -39
- package/main.js +137 -8
- package/package.json +1 -1
- package/preload.js +6 -0
- package/renderer/app.js +236 -15
- package/renderer/index.html +36 -0
- package/renderer/style.css +237 -4
- package/renderer/transcript-view.js +75 -1
- package/vendor/aegis.js +77 -22
- package/vendor/session-store.js +196 -100
- package/vendor/update.js +140 -0
package/lib/local/tools.js
CHANGED
|
@@ -81,14 +81,12 @@ const fail = (error) => ({ ok: false, error: cap(error) });
|
|
|
81
81
|
const SCHEMAS = {
|
|
82
82
|
readFile: {
|
|
83
83
|
name: 'readFile',
|
|
84
|
-
description:
|
|
85
|
-
'Reads a file from the local filesystem. The file_path parameter must be an absolute path. ' +
|
|
86
|
-
'Returns line-numbered content.',
|
|
84
|
+
description: 'Read a file. Absolute path. Returns line-numbered content.',
|
|
87
85
|
parameters: {
|
|
88
86
|
type: 'object',
|
|
89
87
|
properties: {
|
|
90
|
-
file_path: { type: 'string', description: '
|
|
91
|
-
offset: { type: 'number', description: '
|
|
88
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
89
|
+
offset: { type: 'number', description: 'Start line (0-based)' },
|
|
92
90
|
limit: { type: 'number', description: `The number of lines to read (max 10000, default ${READ_LINE_CAP})` },
|
|
93
91
|
},
|
|
94
92
|
required: ['file_path'],
|
|
@@ -98,13 +96,12 @@ const SCHEMAS = {
|
|
|
98
96
|
writeFile: {
|
|
99
97
|
name: 'writeFile',
|
|
100
98
|
description:
|
|
101
|
-
'
|
|
102
|
-
'Use it to create or replace a whole file; it overwrites whatever was there.',
|
|
99
|
+
'Write a whole file, overwriting it. Parent dirs are created. Use editFile to change part of one.',
|
|
103
100
|
parameters: {
|
|
104
101
|
type: 'object',
|
|
105
102
|
properties: {
|
|
106
|
-
file_path: { type: 'string', description: '
|
|
107
|
-
content: { type: 'string', description: '
|
|
103
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
104
|
+
content: { type: 'string', description: 'Full file contents' },
|
|
108
105
|
},
|
|
109
106
|
required: ['file_path', 'content'],
|
|
110
107
|
additionalProperties: false,
|
|
@@ -113,16 +110,15 @@ const SCHEMAS = {
|
|
|
113
110
|
editFile: {
|
|
114
111
|
name: 'editFile',
|
|
115
112
|
description:
|
|
116
|
-
'
|
|
117
|
-
'
|
|
118
|
-
'existing file — it fails loudly on a non-unique or missing match instead of guessing.',
|
|
113
|
+
'Exact string replacement. old_string must be unique unless replace_all. Fails loudly on a ' +
|
|
114
|
+
'missing or ambiguous match rather than guessing.',
|
|
119
115
|
parameters: {
|
|
120
116
|
type: 'object',
|
|
121
117
|
properties: {
|
|
122
|
-
file_path: { type: 'string', description: '
|
|
123
|
-
old_string: { type: 'string', description: '
|
|
124
|
-
new_string: { type: 'string', description: '
|
|
125
|
-
replace_all: { type: 'boolean', description: '
|
|
118
|
+
file_path: { type: 'string', description: 'Absolute path' },
|
|
119
|
+
old_string: { type: 'string', description: 'Text to replace; must be unique unless replace_all' },
|
|
120
|
+
new_string: { type: 'string', description: 'Replacement text' },
|
|
121
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence' },
|
|
126
122
|
},
|
|
127
123
|
required: ['file_path', 'old_string', 'new_string'],
|
|
128
124
|
additionalProperties: false,
|
|
@@ -130,9 +126,7 @@ const SCHEMAS = {
|
|
|
130
126
|
},
|
|
131
127
|
listDir: {
|
|
132
128
|
name: 'listDir',
|
|
133
|
-
description:
|
|
134
|
-
'Lists one directory (non-recursive). Directories are marked with a trailing slash. ' +
|
|
135
|
-
'Skips node_modules, .git and dist.',
|
|
129
|
+
description: 'List one directory (non-recursive). Skips node_modules, .git, dist.',
|
|
136
130
|
parameters: {
|
|
137
131
|
type: 'object',
|
|
138
132
|
properties: {
|
|
@@ -145,12 +139,12 @@ const SCHEMAS = {
|
|
|
145
139
|
glob: {
|
|
146
140
|
name: 'glob',
|
|
147
141
|
description:
|
|
148
|
-
'Find files
|
|
142
|
+
'Find files by glob (**, *, ?). Skips node_modules, .git, dist.',
|
|
149
143
|
parameters: {
|
|
150
144
|
type: 'object',
|
|
151
145
|
properties: {
|
|
152
|
-
pattern: { type: 'string', description: '
|
|
153
|
-
path: { type: 'string', description: '
|
|
146
|
+
pattern: { type: 'string', description: 'Glob, e.g. "**/*.test.js"' },
|
|
147
|
+
path: { type: 'string', description: 'Search root (default: cwd)' },
|
|
154
148
|
},
|
|
155
149
|
required: ['pattern'],
|
|
156
150
|
additionalProperties: false,
|
|
@@ -158,9 +152,7 @@ const SCHEMAS = {
|
|
|
158
152
|
},
|
|
159
153
|
grep: {
|
|
160
154
|
name: 'grep',
|
|
161
|
-
description:
|
|
162
|
-
'Search file contents using a regular expression. Returns file:line matches. ' +
|
|
163
|
-
'Skips node_modules, .git and dist by default.',
|
|
155
|
+
description: 'Search file contents by regex. Returns file:line matches. Skips node_modules, .git, dist.',
|
|
164
156
|
parameters: {
|
|
165
157
|
type: 'object',
|
|
166
158
|
properties: {
|
|
@@ -174,15 +166,13 @@ const SCHEMAS = {
|
|
|
174
166
|
exec: {
|
|
175
167
|
name: 'exec',
|
|
176
168
|
description:
|
|
177
|
-
'
|
|
178
|
-
'stdout+stderr
|
|
179
|
-
'within the same turn — it is a real session, not a fresh process each time. ' +
|
|
180
|
-
'Use for system operations, git commands and package management.',
|
|
169
|
+
'Run a shell command in a PERSISTENT session: cd and exported env carry across calls in ' +
|
|
170
|
+
'this turn. Returns stdout+stderr and the exit code. Use for git, packages, system ops.',
|
|
181
171
|
parameters: {
|
|
182
172
|
type: 'object',
|
|
183
173
|
properties: {
|
|
184
|
-
command: { type: 'string', description: '
|
|
185
|
-
cwd: { type: 'string', description: 'Run this one command
|
|
174
|
+
command: { type: 'string', description: 'Command to run' },
|
|
175
|
+
cwd: { type: 'string', description: 'Run this one command elsewhere; session cwd unchanged' },
|
|
186
176
|
timeout: { type: 'number', description: `Timeout in milliseconds (max ${EXEC_TIMEOUT_CAP}, default ${EXEC_TIMEOUT_DEFAULT})` },
|
|
187
177
|
description: { type: 'string', description: 'A brief description of what the command does (for display)' },
|
|
188
178
|
},
|
|
@@ -193,17 +183,12 @@ const SCHEMAS = {
|
|
|
193
183
|
task: {
|
|
194
184
|
name: 'task',
|
|
195
185
|
description:
|
|
196
|
-
'
|
|
197
|
-
'
|
|
198
|
-
'on the same model and returns a final report as the tool result. Use it to delegate work ' +
|
|
199
|
-
'like scanning for vulnerabilities, reviewing code, planning a refactor, or scaffolding a ' +
|
|
200
|
-
'component — give it a complete, self-contained prompt since it cannot ask follow-up ' +
|
|
201
|
-
'questions. Subagents can delegate further with task, so a large job can be split ' +
|
|
202
|
-
'hierarchically as deep as useful.',
|
|
186
|
+
'Delegate a focused multi-step sub-task to a subagent with its own tool loop; it returns a ' +
|
|
187
|
+
'final report. Give it a complete, self-contained prompt — it cannot ask follow-ups.',
|
|
203
188
|
parameters: {
|
|
204
189
|
type: 'object',
|
|
205
190
|
properties: {
|
|
206
|
-
description: { type: 'string', description: '
|
|
191
|
+
description: { type: 'string', description: 'Short label (3-5 words)' },
|
|
207
192
|
subagent_type: {
|
|
208
193
|
type: 'string',
|
|
209
194
|
enum: [...agentRoles(), 'general'],
|
package/main.js
CHANGED
|
@@ -195,6 +195,44 @@ function errorText(err) {
|
|
|
195
195
|
return err && err.message ? err.message : String(err);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Result shape for the two billing actions (`aegis:billingCheckout` and
|
|
200
|
+
* `aegis:tokenBankTopup`). Both end in a Stripe-hosted checkout URL created
|
|
201
|
+
* server-side, and both have failure modes the user has to be able to tell
|
|
202
|
+
* apart: the free-plan cap (402), no/invalid API key (401), an amount the
|
|
203
|
+
* server rejects (400), Stripe not configured on the server (503
|
|
204
|
+
* `setup_required`), and a plain transport/500 error.
|
|
205
|
+
*
|
|
206
|
+
* The classification happens HERE for the same reason the memory paths
|
|
207
|
+
* normalise: `ipcRenderer.invoke` carries only the message STRING across the
|
|
208
|
+
* boundary, so `err.status` / `err.data` are gone by the time the renderer
|
|
209
|
+
* sees a rejection. The renderer gets a resolved payload instead — never a
|
|
210
|
+
* throw it would have to string-match.
|
|
211
|
+
*/
|
|
212
|
+
async function billingResult(run) {
|
|
213
|
+
try {
|
|
214
|
+
const data = await run();
|
|
215
|
+
return {
|
|
216
|
+
ok: true,
|
|
217
|
+
url: (data && data.url) || null,
|
|
218
|
+
status: 0,
|
|
219
|
+
reason: null,
|
|
220
|
+
setupRequired: false,
|
|
221
|
+
upgrade: null,
|
|
222
|
+
};
|
|
223
|
+
} catch (err) {
|
|
224
|
+
const data = (err && err.data) || {};
|
|
225
|
+
return {
|
|
226
|
+
ok: false,
|
|
227
|
+
url: null,
|
|
228
|
+
status: (err && err.status) || 0,
|
|
229
|
+
reason: errorText(err),
|
|
230
|
+
setupRequired: Boolean(data.setup_required),
|
|
231
|
+
upgrade: upgradeInfo(err),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
198
236
|
/**
|
|
199
237
|
* `aegis:memorySave` with the offline-first fallback (plan P3 §7): try the
|
|
200
238
|
* cloud save first; if it fails (no key, offline, transient error) queue the
|
|
@@ -381,6 +419,15 @@ function createIpcDispatch(aegis, dir, persistApiKey, openExternal) {
|
|
|
381
419
|
verifyApiKey: () => aegis.verifyApiKey(),
|
|
382
420
|
tokenBankBalance: () => aegis.tokenBankBalance(),
|
|
383
421
|
|
|
422
|
+
// Billing (plan upgrade + token-bank top-up). aegis1 creates a Stripe
|
|
423
|
+
// checkout session and answers `{ url }`; the renderer opens that URL
|
|
424
|
+
// through the same aegis:openExternal path the upgrade notices use, so a
|
|
425
|
+
// payment page is never rendered inside the app's own window. Both
|
|
426
|
+
// resolve a shaped result instead of rejecting — see billingResult().
|
|
427
|
+
billingCheckout: () => billingResult(() => aegis.billingCheckout()),
|
|
428
|
+
tokenBankTopup: (payload) =>
|
|
429
|
+
billingResult(() => aegis.tokenBankTopup(payload && payload.amountEur)),
|
|
430
|
+
|
|
384
431
|
listModels: () => aegis.listModels(),
|
|
385
432
|
|
|
386
433
|
chatCompletion: (payload) =>
|
|
@@ -969,6 +1016,79 @@ const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
|
969
1016
|
* matches the "never auto-restart without consent" rule: quitAndInstall()
|
|
970
1017
|
* is likewise only ever invoked by an explicit "Restart to install" click.
|
|
971
1018
|
*/
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Update state backed by the npm registry, for installs that are not packaged
|
|
1022
|
+
* electron builds (i.e. `npm i -g aegis-desktop`).
|
|
1023
|
+
*
|
|
1024
|
+
* Mirrors the electron-updater manager's shape exactly so the IPC dispatch and
|
|
1025
|
+
* the renderer banner need no branch: status/check/download/quitAndInstall/
|
|
1026
|
+
* start. `download` and `quitAndInstall` are no-ops that report the install
|
|
1027
|
+
* command instead of acting — npm owns installation here, and silently
|
|
1028
|
+
* swapping the package under a running process is not something to do on the
|
|
1029
|
+
* user's behalf.
|
|
1030
|
+
*/
|
|
1031
|
+
function createNpmUpdateManager({ onStatus }) {
|
|
1032
|
+
const PKG = 'aegis-desktop';
|
|
1033
|
+
let current = 'unknown';
|
|
1034
|
+
try {
|
|
1035
|
+
current = require('./package.json').version;
|
|
1036
|
+
} catch {
|
|
1037
|
+
/* version is cosmetic here */
|
|
1038
|
+
}
|
|
1039
|
+
let state = { status: 'idle', version: null, error: null, channel: 'npm' };
|
|
1040
|
+
const set = (patch) => {
|
|
1041
|
+
state = { ...state, ...patch };
|
|
1042
|
+
if (onStatus) onStatus(state);
|
|
1043
|
+
return state;
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
// Same repo-then-vendor idiom every other shared module here uses.
|
|
1047
|
+
let updater = null;
|
|
1048
|
+
try {
|
|
1049
|
+
updater = require('../client/update.js');
|
|
1050
|
+
} catch {
|
|
1051
|
+
try {
|
|
1052
|
+
updater = require('./vendor/update.js');
|
|
1053
|
+
} catch {
|
|
1054
|
+
updater = null;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
async function check() {
|
|
1059
|
+
if (!updater) return set({ status: 'disabled', error: 'update checker unavailable' });
|
|
1060
|
+
set({ status: 'checking', error: null });
|
|
1061
|
+
try {
|
|
1062
|
+
const latest = await updater.fetchLatest({ pkg: PKG });
|
|
1063
|
+
if (!latest) return set({ status: 'error', error: 'registry unreachable' });
|
|
1064
|
+
if (!updater.isNewer(latest, current)) return set({ status: 'up-to-date', version: latest });
|
|
1065
|
+
return set({
|
|
1066
|
+
status: 'available',
|
|
1067
|
+
version: latest,
|
|
1068
|
+
command: `npm i -g ${PKG}@latest`,
|
|
1069
|
+
});
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
return set({ status: 'error', error: errorText(err) });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
return {
|
|
1076
|
+
status: () => state,
|
|
1077
|
+
check,
|
|
1078
|
+
// npm installs are the user's to run; saying so beats pretending to act.
|
|
1079
|
+
download: async () => set({
|
|
1080
|
+
status: state.status === 'available' ? 'available' : state.status,
|
|
1081
|
+
command: `npm i -g ${PKG}@latest`,
|
|
1082
|
+
}),
|
|
1083
|
+
quitAndInstall: () => {},
|
|
1084
|
+
start: () => {
|
|
1085
|
+
check().catch(() => {});
|
|
1086
|
+
const t = setInterval(() => check().catch(() => {}), UPDATE_CHECK_INTERVAL_MS);
|
|
1087
|
+
if (t.unref) t.unref();
|
|
1088
|
+
},
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
|
|
972
1092
|
function createUpdateManager({ autoUpdater, isPackaged, onStatus }) {
|
|
973
1093
|
let state = { status: 'idle', version: null, error: null };
|
|
974
1094
|
|
|
@@ -978,14 +1098,23 @@ function createUpdateManager({ autoUpdater, isPackaged, onStatus }) {
|
|
|
978
1098
|
}
|
|
979
1099
|
|
|
980
1100
|
if (!isPackaged || !autoUpdater) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1101
|
+
// Not a packaged build — which is how this app is INSTALLED FROM NPM, not
|
|
1102
|
+
// just how it runs in dev. That case used to resolve a flat 'disabled' and
|
|
1103
|
+
// never check anything, so an npm-installed desktop could never learn a
|
|
1104
|
+
// new version existed.
|
|
1105
|
+
//
|
|
1106
|
+
// electron-updater's configured feed is GitHub Releases
|
|
1107
|
+
// (electron-builder.yml `publish:`), and there has never been a published
|
|
1108
|
+
// release to read — the only one is a draft, invisible to the updater, and
|
|
1109
|
+
// the release workflow cannot run while Actions is billing-locked. So the
|
|
1110
|
+
// feed the packaged path depends on does not exist, while the registry we
|
|
1111
|
+
// actually publish to does.
|
|
1112
|
+
//
|
|
1113
|
+
// npm is therefore the real update channel. Checking it costs one request
|
|
1114
|
+
// a day, reports the version and the exact command, and deliberately does
|
|
1115
|
+
// NOT self-install: replacing your own running process mid-session is a
|
|
1116
|
+
// different promise from telling you an update exists.
|
|
1117
|
+
return createNpmUpdateManager({ onStatus: setState });
|
|
989
1118
|
}
|
|
990
1119
|
|
|
991
1120
|
autoUpdater.autoDownload = false;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.3",
|
|
5
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
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/preload.js
CHANGED
|
@@ -86,6 +86,12 @@ const api = {
|
|
|
86
86
|
setApiKey: (key) => invoke('setApiKey', { key }),
|
|
87
87
|
verifyApiKey: () => invoke('verifyApiKey'),
|
|
88
88
|
tokenBankBalance: () => invoke('tokenBankBalance'),
|
|
89
|
+
// Billing: both start a Stripe-hosted checkout and resolve a shaped
|
|
90
|
+
// `{ ok, url | status, reason, upgrade }` (main.js billingResult). The
|
|
91
|
+
// renderer opens the returned URL with openExternal below — the checkout
|
|
92
|
+
// page itself never loads in this window.
|
|
93
|
+
billingCheckout: () => invoke('billingCheckout'),
|
|
94
|
+
tokenBankTopup: (amountEur) => invoke('tokenBankTopup', { amountEur }),
|
|
89
95
|
listModels: () => invoke('listModels'),
|
|
90
96
|
// Streaming chat (D2.1): pass an onDelta callback to receive SSE chunks as
|
|
91
97
|
// they arrive (pushed from main over aegis:chatDelta). The invoke promise
|