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.
@@ -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: 'The absolute path to the file to read' },
91
- offset: { type: 'number', description: 'The line number to start reading from (0-based)' },
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
- 'Writes a file to the local filesystem. Parent directories are created automatically. ' +
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: 'The absolute path to the file to write' },
107
- content: { type: 'string', description: 'The full contents of the file' },
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
- 'Performs an exact string replacement in a file. old_string must be unique in the file ' +
117
- 'unless replace_all is true. Use this instead of writeFile when changing part of an ' +
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: 'The absolute path to the file to modify' },
123
- old_string: { type: 'string', description: 'The text to replace (must be unique unless replace_all is true)' },
124
- new_string: { type: 'string', description: 'The text to replace it with' },
125
- replace_all: { type: 'boolean', description: 'If true, replace all occurrences of old_string' },
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 matching a glob pattern. Supports **, * and ?. Skips node_modules, .git and dist by default.',
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: 'The glob pattern to match, e.g. "**/*.test.js"' },
153
- path: { type: 'string', description: 'The directory to search from (default: the working directory)' },
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
- 'Executes a shell command in a persistent shell session and returns its combined ' +
178
- 'stdout+stderr plus the exit code. State (cd, exported env vars) carries across calls ' +
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: 'The shell command to execute' },
185
- cwd: { type: 'string', description: 'Run this one command in a different directory without moving the session (the session cwd is unchanged for later calls)' },
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
- 'Spawn a specialized subagent to autonomously handle a focused, multi-step sub-task. ' +
197
- 'The subagent runs its own tool loop (readFile/writeFile/editFile/listDir/glob/grep/exec) ' +
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: 'A short (3-5 word) description of the sub-task' },
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
- const disabledState = { status: 'disabled', version: null, error: null };
982
- return {
983
- status: () => disabledState,
984
- check: () => Promise.resolve(disabledState),
985
- download: () => Promise.resolve(disabledState),
986
- quitAndInstall: () => {},
987
- start: () => {},
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.0",
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