aegis-desktop 0.8.8 → 0.8.10
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 +6 -0
- package/lib/avatar/turn.js +1 -1
- package/lib/local/autonomous.js +2 -2
- package/lib/local/engine.js +91 -39
- package/lib/provider-key-mirror.js +136 -0
- package/main.js +29 -0
- package/package.json +5 -4
- package/renderer/app.js +40 -1
- package/renderer/fonts/4iCs6KVjbNBYlgoKcQ72j00.woff2 +0 -0
- package/renderer/fonts/4iCs6KVjbNBYlgoKcg72j00.woff2 +0 -0
- package/renderer/fonts/4iCs6KVjbNBYlgoKcw72j00.woff2 +0 -0
- package/renderer/fonts/4iCs6KVjbNBYlgoKew72j00.woff2 +0 -0
- package/renderer/fonts/4iCs6KVjbNBYlgoKfA72j00.woff2 +0 -0
- package/renderer/fonts/4iCs6KVjbNBYlgoKfw72.woff2 +0 -0
- package/renderer/fonts/4iCu6KVjbNBYlgoKej70l0k.woff2 +0 -0
- package/renderer/fonts/4iCu6KVjbNBYlgoKej73l0mwFg.woff2 +0 -0
- package/renderer/fonts/4iCu6KVjbNBYlgoKej74l0mwFg.woff2 +0 -0
- package/renderer/fonts/4iCu6KVjbNBYlgoKej75l0mwFg.woff2 +0 -0
- package/renderer/fonts/4iCu6KVjbNBYlgoKej76l0mwFg.woff2 +0 -0
- package/renderer/fonts/4iCu6KVjbNBYlgoKej7wl0mwFg.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCjC3js2yNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jsGyN.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jtGyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jvGyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jvWyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCjC3jvmyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjs2yNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjsGyN.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjtGyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjvGyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjvWyNL4U.woff2 +0 -0
- package/renderer/fonts/4iCv6KVjbNBYlgoCxCvjvmyNL4U.woff2 +0 -0
- package/renderer/fonts/8vIH7w4qzmVxm25L9Hz_.woff2 +0 -0
- package/renderer/fonts/8vIH7w4qzmVxm2BL9A.woff2 +0 -0
- package/renderer/fonts/8vIH7w4qzmVxm2NL9Hz_.woff2 +0 -0
- package/renderer/fonts/NOTICE.md +57 -0
- package/renderer/fonts/aFTR7PB1QTsUX8KYvrGyEY2tbZX9.woff2 +0 -0
- package/renderer/fonts/aFTR7PB1QTsUX8KYvrGyEYOtbQ.woff2 +0 -0
- package/renderer/fonts/aFTR7PB1QTsUX8KYvumzEY2tbZX9.woff2 +0 -0
- package/renderer/fonts/aFTR7PB1QTsUX8KYvumzEYOtbQ.woff2 +0 -0
- package/renderer/fonts/aFTU7PB1QTsUX8KYthSQBLyM.woff2 +0 -0
- package/renderer/fonts/aFTU7PB1QTsUX8KYthqQBA.woff2 +0 -0
- package/renderer/fonts/aFTW7PB1QTsUX8KYth-gBqSI.woff2 +0 -0
- package/renderer/fonts/aFTW7PB1QTsUX8KYth-gCKSIUKw.woff2 +0 -0
- package/renderer/fonts/fonts.css +368 -0
- package/renderer/index.html +6 -7
- package/renderer/quick.css +28 -3
- package/renderer/quick.html +4 -1
- package/renderer/style.css +14 -2
- package/vendor/brain-catalog.js +188 -0
- package/vendor/byok-catalog.js +144 -0
- package/vendor/env-file.js +398 -11
package/README.md
CHANGED
|
@@ -326,3 +326,9 @@ host over the same engine.
|
|
|
326
326
|
[`aegis-desktop`](https://www.npmjs.com/package/aegis-desktop) is built from a
|
|
327
327
|
`git subtree split` of it — same code, published without the rest of the
|
|
328
328
|
monorepo. Edits land here and are synced out; nothing is authored elsewhere.
|
|
329
|
+
|
|
330
|
+
---
|
|
331
|
+
|
|
332
|
+
Built by **Niklas Borneklint** — [aegiscloud.org](https://aegiscloud.org) · [@aegisinfo](https://github.com/aegisinfo)
|
|
333
|
+
|
|
334
|
+
Part of the ÆGIS ecosystem.
|
package/lib/avatar/turn.js
CHANGED
|
@@ -107,7 +107,7 @@ function recallPolicy(caps, opts = {}) {
|
|
|
107
107
|
* - the pooled brain tier (`nexus-brain` and the `-smart`/`-neo` spellings the
|
|
108
108
|
* server serves as aliases), which is not one model at all: the server fans
|
|
109
109
|
* the turn out to reasoning workers plus a synthesis pass. That is the one
|
|
110
|
-
* id the Aegis Cloud class offers (engine.js
|
|
110
|
+
* id the Aegis Cloud class offers (engine.js offerableCatalog), so an
|
|
111
111
|
* Aegis Cloud turn satisfies the floor by construction.
|
|
112
112
|
*/
|
|
113
113
|
const REASONING_MODEL_RE = /^(?:deepseek-(?:v4(?:\.\d+)?-(?:flash|pro)|flash|pro|reasoner)|(?:nexus|aegis)-brain(?:-(?:smart|neo))?)$/;
|
package/lib/local/autonomous.js
CHANGED
|
@@ -84,7 +84,7 @@ function writtenPath(tool) {
|
|
|
84
84
|
* The default AEGIS Cloud model for autonomous work: the pooled brain, which
|
|
85
85
|
* is the tier the server can fan out to multiple reasoning workers and
|
|
86
86
|
* synthesise. `nexus-brain` is the canonical id the catalog itself prefers
|
|
87
|
-
* (the other tier spellings are aliases of it — see
|
|
87
|
+
* (the other tier spellings are aliases of it — see offerableCatalog in
|
|
88
88
|
* engine.js).
|
|
89
89
|
*
|
|
90
90
|
* The model id is only the tier. It is NOT what makes an autonomous task
|
|
@@ -108,7 +108,7 @@ const DEFAULT_MODEL = 'nexus-brain';
|
|
|
108
108
|
* the pool auto-route across whichever providers hold a live key.
|
|
109
109
|
*
|
|
110
110
|
* The accept-list is therefore the pooled-brain tier family — the one entry
|
|
111
|
-
* engine.js's
|
|
111
|
+
* engine.js's offerableCatalog offers for the Aegis Cloud class, plus the
|
|
112
112
|
* `-smart`/`-neo` tier spellings the server still serves as aliases of it. This
|
|
113
113
|
* mirrors selectBrainEntry() there rather than re-deriving "anything starting
|
|
114
114
|
* with nexus-": `nexus-fast` is not a tier the catalog has ever served, and
|
package/lib/local/engine.js
CHANGED
|
@@ -76,6 +76,63 @@ try {
|
|
|
76
76
|
}
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* The shared pooled-brain selection rule (`client/brain-catalog.js`), resolved
|
|
81
|
+
* the same two ways as every other shared module: repo-relative from a source
|
|
82
|
+
* checkout, and this app's own staged tree (`desktop/vendor/brain-catalog.js`,
|
|
83
|
+
* flattened by scripts/predist.mjs because a packaged app cannot reach outside
|
|
84
|
+
* its own directory). The CLI resolves the same file its own way, through
|
|
85
|
+
* `cli/src/sharedpaths.js`. Both hosts must agree on which entry stands for the
|
|
86
|
+
* pool, so there is deliberately no fallback implementation here — a silently
|
|
87
|
+
* divergent local copy is the exact bug this module removes. When neither
|
|
88
|
+
* layout has it, require() throws at load time naming the file, rather than the
|
|
89
|
+
* host quietly offering a different model list than its sibling.
|
|
90
|
+
*/
|
|
91
|
+
function requireSharedBrain() {
|
|
92
|
+
const candidates = [
|
|
93
|
+
() => require('../../../client/brain-catalog.js'),
|
|
94
|
+
() => require('../../vendor/brain-catalog.js'),
|
|
95
|
+
];
|
|
96
|
+
let last = null;
|
|
97
|
+
for (const load of candidates) {
|
|
98
|
+
try {
|
|
99
|
+
return load();
|
|
100
|
+
} catch (err) {
|
|
101
|
+
last = err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw last;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The shared BYOK model additions (`client/byok-catalog.js`), resolved exactly
|
|
109
|
+
* the two ways above and for the same reason: the CLI vendors THIS file
|
|
110
|
+
* (cli/scripts/predist.mjs) and requires the same module through
|
|
111
|
+
* `cli/src/sharedpaths.js`, so a copy-per-host here is how the terminal and the
|
|
112
|
+
* window would come to offer one account two different provider model lists —
|
|
113
|
+
* the defect class this repo keeps paying for.
|
|
114
|
+
*
|
|
115
|
+
* The module is pure and returns a NEW array, so a caller may apply it to a
|
|
116
|
+
* payload it did not build. It never invents a provider: a provider absent from
|
|
117
|
+
* the server's catalog stays absent, because a row that accepts a vendor key
|
|
118
|
+
* must correspond to a slug the relay actually takes.
|
|
119
|
+
*/
|
|
120
|
+
function requireSharedByokCatalog() {
|
|
121
|
+
const candidates = [
|
|
122
|
+
() => require('../../../client/byok-catalog.js'),
|
|
123
|
+
() => require('../../vendor/byok-catalog.js'),
|
|
124
|
+
];
|
|
125
|
+
let last = null;
|
|
126
|
+
for (const load of candidates) {
|
|
127
|
+
try {
|
|
128
|
+
return load();
|
|
129
|
+
} catch (err) {
|
|
130
|
+
last = err;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
throw last;
|
|
134
|
+
}
|
|
135
|
+
|
|
79
136
|
/**
|
|
80
137
|
* How long a turn waits for the working-tree lock before running anyway.
|
|
81
138
|
*
|
|
@@ -244,48 +301,36 @@ function normalizeCatalog(models) {
|
|
|
244
301
|
}
|
|
245
302
|
|
|
246
303
|
/**
|
|
247
|
-
* The Aegis Cloud
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
304
|
+
* The Aegis Cloud (pooled) dropdown offers Nexus, and only Nexus.
|
|
305
|
+
*
|
|
306
|
+
* The catalog (`/api/v1/models`) lists per-provider ids (`deepseek`,
|
|
307
|
+
* `anthropic`, `groq`, `openai`, ...) alongside five pooled-brain tier
|
|
308
|
+
* spellings (`{aegis,nexus}-brain[-smart|-neo]`) that all route the same
|
|
309
|
+
* worker pool. This host briefly offered the full list — the per-provider ids
|
|
310
|
+
* included — on the theory that a funded account should be able to pin the
|
|
311
|
+
* exact model it pays for. Niklas's explicit instruction is narrower: AEGIS
|
|
312
|
+
* Cloud offers Nexus only, no per-provider pin, so this is back on
|
|
313
|
+
* `filterAegisCatalog` — the shared rule's single-row collapse.
|
|
314
|
+
*
|
|
315
|
+
* The rule itself lives in `client/brain-catalog.js`, shared with the CLI, so
|
|
316
|
+
* there is exactly one copy of it: two copies is how this host and the
|
|
317
|
+
* terminal previously came to offer the same account two different model
|
|
318
|
+
* lists. `filterAegisCatalog` collapses the five brain-tier spellings to the
|
|
319
|
+
* one row labelled Nexus; a payload with no brain tier (the BYOK listing) is
|
|
320
|
+
* unaffected — see the desktop's byok branch below, which still offers every
|
|
321
|
+
* provider id the account holds a key for. Resolved the two ways this repo
|
|
322
|
+
* resolves every shared module — repo-relative in a checkout, and this app's
|
|
323
|
+
* staged vendor/ tree.
|
|
257
324
|
*/
|
|
258
|
-
const
|
|
259
|
-
const NEXUS_LABEL = 'Nexus';
|
|
325
|
+
const { filterAegisCatalog } = requireSharedBrain();
|
|
260
326
|
|
|
261
327
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* request travels on the name the server owns; fall back to the alias, then to
|
|
267
|
-
* any tier whose `alias_of` names the brain, so a renamed or trimmed catalog
|
|
268
|
-
* still resolves to something selectable instead of silently emptying the
|
|
269
|
-
* dropdown (the previous fixed-id lookup returned [] if `aegis-brain` was ever
|
|
270
|
-
* retired, leaving the class with no model to choose).
|
|
328
|
+
* The BYOK additions, applied to the server's provider catalog below. Same
|
|
329
|
+
* one-copy rule as the brain rule above: the terminal requires this same module
|
|
330
|
+
* (cli/src/commands.js, through sharedpaths.js), so neither host can offer a
|
|
331
|
+
* provider row the other does not.
|
|
271
332
|
*/
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
models.find((m) => NEXUS_BRAIN_IDS.includes(m.id) && !m.hidden && m.alias_of === undefined)
|
|
275
|
-
|| models.find((m) => NEXUS_BRAIN_IDS.includes(m.id))
|
|
276
|
-
|| models.find((m) => m.alias_of && NEXUS_BRAIN_IDS.includes(m.alias_of))
|
|
277
|
-
|| null
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function filterAegisCatalog(models) {
|
|
282
|
-
const nexus = selectBrainEntry(models);
|
|
283
|
-
if (!nexus) return [];
|
|
284
|
-
// Drop the alias bookkeeping: this entry *is* the selection, so the renderer
|
|
285
|
-
// must never treat it as a hidden alias and filter it back out.
|
|
286
|
-
const { hidden, alias_of, ...rest } = nexus;
|
|
287
|
-
return [{ ...rest, label: NEXUS_LABEL }];
|
|
288
|
-
}
|
|
333
|
+
const { withByokAdditions } = requireSharedByokCatalog();
|
|
289
334
|
|
|
290
335
|
// ── Agent-loop helpers ──────────────────────────────────────────────────────
|
|
291
336
|
|
|
@@ -790,7 +835,14 @@ function createLocalEngine({
|
|
|
790
835
|
let fee = null;
|
|
791
836
|
try {
|
|
792
837
|
const data = await aegis.byokProviders();
|
|
793
|
-
|
|
838
|
+
// The server's catalog, plus the ids it does not name but the vendor
|
|
839
|
+
// serves and the relay will forward (`client/byok-catalog.js`, shared
|
|
840
|
+
// with the CLI so both hosts offer the same rows). Applied HERE, at the
|
|
841
|
+
// single point where the payload enters this engine, so every consumer
|
|
842
|
+
// of `providers` — the model list built just below, the renderer's
|
|
843
|
+
// per-provider Settings rows, and the CLI's own `/byok` listing when it
|
|
844
|
+
// runs on this engine — sees one list instead of three opinions.
|
|
845
|
+
providers = withByokAdditions((data && data.providers) || []);
|
|
794
846
|
// The handling fee AEGIS adds on top of the caller's vendor bill. It is
|
|
795
847
|
// the server's own published rate (services/pricing.price_byok_call) and
|
|
796
848
|
// is passed through untouched — never re-derived here, because a client
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* provider-key-mirror.js — make a provider key saved in the DESKTOP visible to
|
|
5
|
+
* every other host on the same machine.
|
|
6
|
+
*
|
|
7
|
+
* The desktop's Provider settings rows persist into its own encrypted settings
|
|
8
|
+
* store (`desktop/lib/settings.js` -> `settings.json` under the app's userData
|
|
9
|
+
* dir). That store is private to this app: `aegiscode` (the CLI) and the MCP
|
|
10
|
+
* plugin never read it, and they cannot — it is an encrypted Electron store in
|
|
11
|
+
* another process's data directory. So a key added in the desktop was invisible
|
|
12
|
+
* to the CLI on the next run: same machine, same provider, key asked for twice.
|
|
13
|
+
*
|
|
14
|
+
* The fix is not to teach every host to open an encrypted Electron store. It is
|
|
15
|
+
* to write the key where all four hosts already look at start-up —
|
|
16
|
+
* `~/.aegiscode/.env`, through the ONE shared writer (`client/env-file.js`
|
|
17
|
+
* `setEnvValue`). After this, "add your provider key in the app" and "put it in
|
|
18
|
+
* ~/.aegiscode/.env" are the same instruction, which is the point.
|
|
19
|
+
*
|
|
20
|
+
* Policy, deliberately narrow:
|
|
21
|
+
*
|
|
22
|
+
* - PROVIDER keys only. The AEGIS *account* key is already shared through
|
|
23
|
+
* `credentials.json` (`client/credentials.js`), which the CLI, the MCP
|
|
24
|
+
* plugin and the desktop all read. A second copy in the env file would be a
|
|
25
|
+
* second authority that can silently disagree with the first — exactly the
|
|
26
|
+
* "which one won?" failure env-file.js refuses for duplicate lines. So
|
|
27
|
+
* `setAegisKey()` is deliberately NOT wrapped.
|
|
28
|
+
* - The variable name comes from `envFile.envVarFor(provider)`, the same
|
|
29
|
+
* mapping the CLI's model picker uses (`keyForModelId`), and must end in
|
|
30
|
+
* `_API_KEY`. A name we cannot map is a name we do not invent: an
|
|
31
|
+
* unrecognised provider leaves the file untouched rather than growing an
|
|
32
|
+
* unrelated variable.
|
|
33
|
+
* - Removing a key only touches the file if the file actually carries that
|
|
34
|
+
* variable, so "Remove" on a provider the file never knew about does not
|
|
35
|
+
* add a stray `OPENAI_API_KEY=` line to a hand-written file.
|
|
36
|
+
* - A failure to mirror is REPORTED and swallowed. The key IS saved (the
|
|
37
|
+
* encrypted store has it); only the cross-host copy failed, and refusing
|
|
38
|
+
* the whole save over that would throw away the key the user just typed.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/** Reserved namespaces (`__aegis`, `__quickLauncher`, …) are the store's own
|
|
42
|
+
* bookkeeping, not providers — never a key variable. */
|
|
43
|
+
const RESERVED_PREFIX = '__';
|
|
44
|
+
|
|
45
|
+
/** Every provider key variable in this product family ends this way. */
|
|
46
|
+
const KEY_VAR_SUFFIX = /_API_KEY$/;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Write one provider key into the shared env file.
|
|
50
|
+
*
|
|
51
|
+
* @param {object} envFile client/env-file.js (injected: this module must stay
|
|
52
|
+
* unit-testable without touching the real home dir)
|
|
53
|
+
* @param {string} provider provider id, e.g. 'openai'
|
|
54
|
+
* @param {string} key the key; '' means "clear it"
|
|
55
|
+
* @param {object} [opts] `onWarn(message)`, `envOpts` forwarded to env-file
|
|
56
|
+
* (its `{ file, env }` seam, used by tests)
|
|
57
|
+
* @returns {object|null} env-file's own result, or null when nothing was
|
|
58
|
+
* mirrored (no mapping, reserved row, absent variable)
|
|
59
|
+
*/
|
|
60
|
+
function mirrorProviderKeyToEnvFile(envFile, provider, key, opts = {}) {
|
|
61
|
+
const warn = typeof opts.onWarn === 'function' ? opts.onWarn : () => {};
|
|
62
|
+
const envOpts = opts.envOpts || {};
|
|
63
|
+
if (!envFile || typeof envFile.setEnvValue !== 'function') return null;
|
|
64
|
+
if (typeof provider !== 'string' || !provider) return null;
|
|
65
|
+
if (provider.startsWith(RESERVED_PREFIX)) return null;
|
|
66
|
+
|
|
67
|
+
const name = typeof envFile.envVarFor === 'function' ? envFile.envVarFor(provider) : '';
|
|
68
|
+
if (!name || !KEY_VAR_SUFFIX.test(name)) return null;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
if (key) return envFile.setEnvValue(name, key, envOpts);
|
|
72
|
+
const status =
|
|
73
|
+
typeof envFile.keyStatus === 'function' ? envFile.keyStatus(name, envOpts) : null;
|
|
74
|
+
if (!status || !status.inFile) return null;
|
|
75
|
+
if (typeof envFile.clearEnvValue === 'function') {
|
|
76
|
+
return envFile.clearEnvValue(name, envOpts);
|
|
77
|
+
}
|
|
78
|
+
// An older vendored env-file without a remover. Writing an empty value is
|
|
79
|
+
// NOT a removal — `setEnvValue` refuses an empty value outright, so that
|
|
80
|
+
// call returned an error and left the stale line in place. Unset the live
|
|
81
|
+
// variable and say plainly that the file still carries it.
|
|
82
|
+
if (envOpts.env) delete envOpts.env[name];
|
|
83
|
+
else delete process.env[name];
|
|
84
|
+
warn(
|
|
85
|
+
`removed ${provider}'s key in the app, but ${name} is still in the shared env file — `
|
|
86
|
+
+ 'this build has no clearEnvValue; delete the line by hand',
|
|
87
|
+
);
|
|
88
|
+
return null;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
warn(`could not mirror ${provider}'s key into the shared env file: ${e && e.message}`);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Wrap a provider-settings store so every key saved or removed through it is
|
|
97
|
+
* also written to the shared env file.
|
|
98
|
+
*
|
|
99
|
+
* Wrapping the store (rather than each caller) is the whole point: the Settings
|
|
100
|
+
* pane, the welcome flow's provider rows and any future caller all funnel
|
|
101
|
+
* through `store.set` / `store.remove`, so there is exactly one place that can
|
|
102
|
+
* forget to mirror — and this is it.
|
|
103
|
+
*
|
|
104
|
+
* @param {object} store a settings store (settings.get/set/remove/list)
|
|
105
|
+
* @param {object} envFile client/env-file.js
|
|
106
|
+
* @param {object} [opts] `onWarn`, `envOpts` (see above)
|
|
107
|
+
* @returns {object} the same store, wrapped
|
|
108
|
+
*/
|
|
109
|
+
function wrapProviderKeyMirror(store, envFile, opts = {}) {
|
|
110
|
+
if (!store || typeof store.set !== 'function') return store;
|
|
111
|
+
const mirror = (provider, key) => mirrorProviderKeyToEnvFile(envFile, provider, key, opts);
|
|
112
|
+
|
|
113
|
+
const rawSet = store.set.bind(store);
|
|
114
|
+
const rawRemove = typeof store.remove === 'function' ? store.remove.bind(store) : null;
|
|
115
|
+
|
|
116
|
+
store.set = (provider, cfg) => {
|
|
117
|
+
const res = rawSet(provider, cfg);
|
|
118
|
+
// A row with no key is a base-URL/local-daemon row: there is no secret to
|
|
119
|
+
// share, and mirroring would write an empty variable into a file every host
|
|
120
|
+
// sources. `cfg.key` undefined OR empty both mean "no key supplied"
|
|
121
|
+
// (a blank field in the pane means "keep the stored one").
|
|
122
|
+
if (cfg && typeof cfg.key === 'string' && cfg.key) mirror(provider, cfg.key);
|
|
123
|
+
return res;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (rawRemove) {
|
|
127
|
+
store.remove = (provider) => {
|
|
128
|
+
const res = rawRemove(provider);
|
|
129
|
+
mirror(provider, '');
|
|
130
|
+
return res;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return store;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { mirrorProviderKeyToEnvFile, wrapProviderKeyMirror };
|
package/main.js
CHANGED
|
@@ -2600,6 +2600,31 @@ function bootstrap() {
|
|
|
2600
2600
|
dir: dataDir,
|
|
2601
2601
|
});
|
|
2602
2602
|
|
|
2603
|
+
// A provider key typed in the Settings pane used to reach ONLY this app's
|
|
2604
|
+
// encrypted store, so `aegiscode` (the CLI) and the MCP plugin never saw it:
|
|
2605
|
+
// same machine, same provider, key asked for twice, and a key entered here
|
|
2606
|
+
// was gone from every other host on the next run. Mirror every key write into
|
|
2607
|
+
// the shared `~/.aegiscode/.env` — the one file all four hosts read at
|
|
2608
|
+
// start-up (loadEnvFile() above; cli/bin/aegiscode.js before the CLI runs) —
|
|
2609
|
+
// so "add the key in the app" and "put it in ~/.aegiscode/.env" are the same
|
|
2610
|
+
// instruction. The encrypted store stays this app's authority; the file is
|
|
2611
|
+
// what makes the key exist for the others. The AEGIS *account* key is
|
|
2612
|
+
// deliberately not mirrored: it is already shared via credentials.json, and a
|
|
2613
|
+
// second copy is a second authority that can disagree (see the module's
|
|
2614
|
+
// header). Wrapped before registerModelIpc below, so every IPC path uses the
|
|
2615
|
+
// wrapped store.
|
|
2616
|
+
try {
|
|
2617
|
+
const { wrapProviderKeyMirror } = require('./lib/provider-key-mirror.js');
|
|
2618
|
+
const onWarn = (m) => console.warn(`aegis: ${m}`);
|
|
2619
|
+
wrapProviderKeyMirror(settings, envFile, { onWarn });
|
|
2620
|
+
if (engine && engine.settings && engine.settings !== settings) {
|
|
2621
|
+
wrapProviderKeyMirror(engine.settings, envFile, { onWarn });
|
|
2622
|
+
}
|
|
2623
|
+
} catch (e) {
|
|
2624
|
+
// Never fatal: the store still works, only the cross-host copy is missing.
|
|
2625
|
+
console.warn(`aegis: could not wire the shared env-file key mirror: ${e && e.message}`);
|
|
2626
|
+
}
|
|
2627
|
+
|
|
2603
2628
|
// Persist the in-app AEGIS key in its own reserved namespace, encrypted —
|
|
2604
2629
|
// never as a provider named 'aegis' (that coupling let the Settings pane's
|
|
2605
2630
|
// "Remove" delete the AEGIS key; defect #1).
|
|
@@ -2610,6 +2635,10 @@ function bootstrap() {
|
|
|
2610
2635
|
// `savedAt` stamps so whichever host wrote last is the key in force.
|
|
2611
2636
|
const persistApiKey = (key) => {
|
|
2612
2637
|
const result = settings.setAegisKey(key);
|
|
2638
|
+
// NOT mirrored into ~/.aegiscode/.env on purpose — the line below already
|
|
2639
|
+
// shares this key with every host (credentials.json, 0600, read by the CLI,
|
|
2640
|
+
// the MCP plugin and this app). Two stores for one account key is two
|
|
2641
|
+
// answers to "which key is in force"; see desktop/lib/provider-key-mirror.js.
|
|
2613
2642
|
if (key) credentials.saveApiKey(key);
|
|
2614
2643
|
else credentials.clearApiKey();
|
|
2615
2644
|
return result;
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.8.
|
|
5
|
-
"description": "Thin Electron host for AEGIS
|
|
4
|
+
"version": "0.8.10",
|
|
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",
|
|
8
8
|
"email": "nborneklint@gmail.com"
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"demo": "node scripts/record-demo-gif.mjs",
|
|
33
33
|
"dist": "npm run predist && electron-builder",
|
|
34
34
|
"dist:dir": "npm run predist && electron-builder --dir",
|
|
35
|
-
"check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.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/local/queue.js && node --check lib/local/autonomous.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
|
|
35
|
+
"check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/budget.js && node --check renderer/usage.js && node --check renderer/stream-policy.js && node --check renderer/transcript-view.js && node --check renderer/diff.js && node --check renderer/diffview.js && node --check renderer/markdown.js && node --check renderer/vendor/aegis-highlight.js && node --check scripts/predist.mjs && node --check scripts/generate-icon.mjs && node --check scripts/bundle-fonts.mjs && node --check lib/local/local.js && node --check lib/local/context.js && node --check lib/local/engine.js && node --check lib/local/session-rounds.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/local/queue.js && node --check lib/local/autonomous.js && node --check lib/settings.js && node --check lib/sync/sessions.js && node --check lib/sync/memory-queue.js && node --check lib/window-state.js && node --check lib/deep-link.js && node --check lib/quick-launcher.js && node --check lib/avatar/tiers.js && node --check lib/avatar/xp.js && node --check lib/avatar/level.js && node --check lib/avatar/events.js && node --check lib/avatar/persona.js && node --check bin/aegis.js",
|
|
36
36
|
"test:shell": "node ../test/desktop-shell.mjs",
|
|
37
37
|
"test:model": "node ../test/model-dispatch.mjs",
|
|
38
38
|
"test:budget": "node ../test/budget.test.mjs",
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"test:avatar-level": "node test/avatar-level.test.mjs",
|
|
53
53
|
"test:avatar-events": "node test/avatar-events.test.mjs",
|
|
54
54
|
"test:avatar-tiers": "node test/avatar-tiers.test.mjs",
|
|
55
|
-
"test:avatar-persona": "node test/avatar-persona.test.mjs"
|
|
55
|
+
"test:avatar-persona": "node test/avatar-persona.test.mjs",
|
|
56
|
+
"test:renderer-fonts": "node test/renderer-fonts.test.mjs"
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"electron-updater": "^6.8.9"
|
package/renderer/app.js
CHANGED
|
@@ -2236,6 +2236,38 @@ function applyWelcomeConnect() {
|
|
|
2236
2236
|
box.hidden = dismissed || keyConfigured === true || providerConfigured === true;
|
|
2237
2237
|
}
|
|
2238
2238
|
|
|
2239
|
+
/**
|
|
2240
|
+
* Land the user on the ONE provider row a just-picked byok model needs a key
|
|
2241
|
+
* for — the per-selection counterpart to `welcomeByok()`'s whole-class reveal.
|
|
2242
|
+
*
|
|
2243
|
+
* Before this, picking an unconfigured row in the model dropdown (deliberately
|
|
2244
|
+
* still offered — see `loadModels()`'s `needsProviderKey` hint, which engine.js
|
|
2245
|
+
* documents as "the catalog answers with no key at all, hiding the row would
|
|
2246
|
+
* hide the answer to which key to get") did nothing: the class-level hint only
|
|
2247
|
+
* fires when NO provider is configured, so a machine with one working key and
|
|
2248
|
+
* nine unconfigured ones got no help picking a row among the nine — just a
|
|
2249
|
+
* turn that fails downstream at the relay with no picker-side context. This
|
|
2250
|
+
* is the CLI's `collectKeyFor` (chatflow.js), for this host: same question
|
|
2251
|
+
* (does this row need a key this machine does not have), answered by pointing
|
|
2252
|
+
* at where to type it rather than reading it inline, because that is how this
|
|
2253
|
+
* app's Settings pane already works for every other key.
|
|
2254
|
+
*
|
|
2255
|
+
* `dataset.provider` on a byok row is `byok:<id>` (see the Provider settings
|
|
2256
|
+
* build loop above), not the bare provider id `modelMeta` rows carry.
|
|
2257
|
+
*/
|
|
2258
|
+
function focusProviderKeyRow(provider) {
|
|
2259
|
+
if (!els.settingsList || !provider) return;
|
|
2260
|
+
const row = els.settingsList.querySelector(
|
|
2261
|
+
`.setting-row[data-provider="byok:${provider}"] input[type="password"]`
|
|
2262
|
+
);
|
|
2263
|
+
revealSidebarCard(els.settingsList);
|
|
2264
|
+
if (row) row.focus();
|
|
2265
|
+
if (els.modelHint) {
|
|
2266
|
+
const base = els.modelHint.textContent.replace(/ — no .* key saved; add it below\.$/, '');
|
|
2267
|
+
els.modelHint.textContent = `${base} — no ${provider} key saved; add it below.`;
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2239
2271
|
/** Scroll a sidebar card into view and mark it, so a welcome click has a
|
|
2240
2272
|
* visible landing spot. The sidebar is a plain scroll container, so this is
|
|
2241
2273
|
* the whole of the "open settings" affordance. */
|
|
@@ -4088,13 +4120,20 @@ async function init() {
|
|
|
4088
4120
|
});
|
|
4089
4121
|
|
|
4090
4122
|
els.modelSelect.addEventListener('change', () => {
|
|
4123
|
+
const meta = modelMeta.get(els.modelSelect.value);
|
|
4091
4124
|
// Display-only (see budget.js): the model's own advertised output limit,
|
|
4092
4125
|
// which is never the number this app puts on a request.
|
|
4093
|
-
const ceiling = maxTokensCeiling(
|
|
4126
|
+
const ceiling = maxTokensCeiling(meta);
|
|
4094
4127
|
const base = els.modelHint.textContent.replace(/ · max output: [\d,]+$/, '');
|
|
4095
4128
|
els.modelHint.textContent =
|
|
4096
4129
|
ceiling < FLAT_CEILING ? `${base} · max output: ${ceiling.toLocaleString()}` : base;
|
|
4097
4130
|
updateBudgetControls(els.classSelect.value);
|
|
4131
|
+
// Picking a byok row this machine holds no key for: point at where the
|
|
4132
|
+
// key goes, right now, instead of a turn that fails at the relay with no
|
|
4133
|
+
// picker-side context. See focusProviderKeyRow's doc for why this exists.
|
|
4134
|
+
if (els.classSelect.value === 'byok' && meta && meta.provider && meta.configured === false) {
|
|
4135
|
+
focusProviderKeyRow(meta.provider);
|
|
4136
|
+
}
|
|
4098
4137
|
});
|
|
4099
4138
|
|
|
4100
4139
|
// The typed model tag IS the selection while the box is visible (see
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Vendored webfonts
|
|
2
|
+
|
|
3
|
+
Committed verbatim, like `renderer/vendor/` — no CDN and no network at run time.
|
|
4
|
+
Loaded by `renderer/index.html` and `renderer/quick.html` under a
|
|
5
|
+
`font-src 'self'` CSP.
|
|
6
|
+
|
|
7
|
+
Generated by `desktop/scripts/bundle-fonts.mjs`; do not edit the `.woff2` files
|
|
8
|
+
or `fonts.css` by hand. To refresh the set:
|
|
9
|
+
|
|
10
|
+
node desktop/scripts/bundle-fonts.mjs # rewrite
|
|
11
|
+
node desktop/scripts/bundle-fonts.mjs --check # verify against the CDN (needs network)
|
|
12
|
+
|
|
13
|
+
All three families are licensed under the **SIL Open Font License 1.1**, which
|
|
14
|
+
permits bundling and redistribution. The copyright lines Google Fonts serves
|
|
15
|
+
with them:
|
|
16
|
+
|
|
17
|
+
- `Syne` — © 2018 The Syne Project Authors
|
|
18
|
+
(<https://github.com/bonjour-monde/fonderie>), OFL 1.1. Display face used for
|
|
19
|
+
the wordmark, headings and UI chrome (`--font-display`).
|
|
20
|
+
- `DM Mono` — © 2020 The DM Mono Project Authors
|
|
21
|
+
(<https://github.com/googlefonts/dm-mono>), OFL 1.1. Monospace face for code,
|
|
22
|
+
diffs and metrics (`--font-mono`).
|
|
23
|
+
- `Ubuntu` — © 2010-2011 Canonical Ltd.
|
|
24
|
+
(<https://github.com/googlefonts/UbuntuFont>), OFL 1.1. Chat body text
|
|
25
|
+
(`--font-chat`).
|
|
26
|
+
|
|
27
|
+
The OFL is a per-file licence; the full text ships with each upstream project at
|
|
28
|
+
the links above and is not duplicated here.
|
|
29
|
+
|
|
30
|
+
## Why these are vendored rather than fetched
|
|
31
|
+
|
|
32
|
+
The `<link>` to `fonts.googleapis.com` that used to sit in `index.html` made the
|
|
33
|
+
app's typography depend on the network and on the host. Online it drew the three
|
|
34
|
+
faces above. Offline — or behind a blocker, or on a machine whose font resolver
|
|
35
|
+
disagreed — the fallback chain `'Segoe UI', system-ui, sans-serif` resolved to a
|
|
36
|
+
*different* font on each platform: SF Pro on macOS, Segoe UI on Windows, whatever
|
|
37
|
+
fontconfig offered on Linux. Three visibly different products from one stylesheet.
|
|
38
|
+
|
|
39
|
+
Serving the same bytes from the app directory makes the rendering identical on
|
|
40
|
+
every platform and lets the CSP drop its external origins entirely. This is the
|
|
41
|
+
desktop counterpart of the terminal-side policy in `cli/src/caps.js`, which
|
|
42
|
+
likewise refuses to let the host change what is drawn.
|
|
43
|
+
|
|
44
|
+
Note the honest limits. These three families cover latin, latin-ext, cyrillic,
|
|
45
|
+
cyrillic-ext, greek and greek-ext — and **not** vietnamese: Google Fonts serves
|
|
46
|
+
no vietnamese subset for Syne, DM Mono or Ubuntu, and `latin-ext` stops at
|
|
47
|
+
U+1E9F and resumes at U+1EF2, so the core Vietnamese diacritic block
|
|
48
|
+
U+1EA0–U+1EF1 falls through to a host font. `desktop/scripts/bundle-fonts.mjs`
|
|
49
|
+
records the same gap, and `desktop/test/renderer-fonts.test.mjs` asserts it, so
|
|
50
|
+
the day Google adds the subset the test fails loudly instead of the gap
|
|
51
|
+
silently persisting.
|
|
52
|
+
|
|
53
|
+
Glyphs outside the covered set — CJK, and colour emoji in particular — are also
|
|
54
|
+
still drawn by a host font and remain platform-specific. `Apple Color Emoji`,
|
|
55
|
+
`Segoe UI Emoji` and `Noto Color Emoji` are not interchangeable and are not
|
|
56
|
+
redistributable here, so that divergence cannot be removed by bundling; it is
|
|
57
|
+
inherent to the platform.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|