aegis-desktop 1.0.10 → 1.0.13
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/auto.js +11 -7
- package/lib/fingerprint.js +76 -17
- package/lib/google-connect.js +171 -0
- package/lib/local/claude-pro.js +134 -11
- package/lib/local/engine.js +111 -26
- package/lib/local/prompt.js +67 -37
- package/lib/local/tools.js +49 -13
- package/lib/settings.js +65 -24
- package/main.js +270 -36
- package/package.json +5 -2
- package/preload.js +45 -12
- package/renderer/app.js +691 -22
- package/renderer/budget.js +18 -9
- package/renderer/companion.js +225 -0
- package/renderer/fingerprint.css +16 -0
- package/renderer/fingerprint.js +136 -2
- package/renderer/index.html +302 -121
- package/renderer/provider-toolbar.js +136 -0
- package/renderer/providers.js +261 -0
- package/renderer/style.css +547 -171
- package/vendor/aegis.js +169 -3
- package/vendor/credentials.js +9 -2
- package/vendor/drive-connect.js +87 -0
- package/vendor/drive-upload.js +176 -0
- package/vendor/google-signin.js +127 -0
- package/vendor/record-vault.js +431 -113
- package/vendor/update.js +366 -8
package/main.js
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
const path = require('node:path');
|
|
28
28
|
const fs = require('node:fs');
|
|
29
|
+
const { execFile } = require('node:child_process');
|
|
29
30
|
const { pathToFileURL } = require('node:url');
|
|
30
31
|
|
|
31
32
|
// Dev / CI / smoke-test layout: desktop/ lives inside the repo, so the shared
|
|
@@ -88,6 +89,30 @@ try {
|
|
|
88
89
|
sharedTopup = require('./vendor/topup.js');
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
// Google identity + Drive (client/google-signin.js, client/drive-connect.js,
|
|
93
|
+
// client/drive-upload.js). google-signin.js already existed for the CLI's
|
|
94
|
+
// `connect --google`; this is the first time desktop wires any of it. See
|
|
95
|
+
// lib/google-connect.js for how the three are sequenced.
|
|
96
|
+
let googleSignIn;
|
|
97
|
+
try {
|
|
98
|
+
googleSignIn = require('../client/google-signin.js');
|
|
99
|
+
} catch {
|
|
100
|
+
googleSignIn = require('./vendor/google-signin.js');
|
|
101
|
+
}
|
|
102
|
+
let driveConnectLib;
|
|
103
|
+
try {
|
|
104
|
+
driveConnectLib = require('../client/drive-connect.js');
|
|
105
|
+
} catch {
|
|
106
|
+
driveConnectLib = require('./vendor/drive-connect.js');
|
|
107
|
+
}
|
|
108
|
+
let driveUploadLib;
|
|
109
|
+
try {
|
|
110
|
+
driveUploadLib = require('../client/drive-upload.js');
|
|
111
|
+
} catch {
|
|
112
|
+
driveUploadLib = require('./vendor/drive-upload.js');
|
|
113
|
+
}
|
|
114
|
+
const { createGoogleConnect } = require('./lib/google-connect.js');
|
|
115
|
+
|
|
91
116
|
// The device fingerprint (client/device-fingerprint.js) — the SAME identity the
|
|
92
117
|
// auto-provision path registers a device-key against, now also carried in every
|
|
93
118
|
// presence beat so one laptop is one machine in the admin panel rather than one
|
|
@@ -109,13 +134,14 @@ try {
|
|
|
109
134
|
// below (`fingerprintStatus`/`keygen`/`mint`/`verify`/`backup`).
|
|
110
135
|
const { createFingerprintService } = require('./lib/fingerprint.js');
|
|
111
136
|
|
|
112
|
-
// The two
|
|
113
|
-
// seal-on-quiet auto-mint. Both are DEFAULT
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
137
|
+
// The two background automations (lib/auto.js): the auto-drain timer and the
|
|
138
|
+
// seal-on-quiet auto-mint. Both are DEFAULT ON — the queue and the seal run
|
|
139
|
+
// with no click and no card on the desktop — and both read their switch from
|
|
140
|
+
// the settings store's reserved `__autoDrain`/`__autoMint` namespaces on every
|
|
141
|
+
// tick, so this require wires the TIMING only: the queue worker still owns the
|
|
142
|
+
// lock and lib/fingerprint.js still owns the seal. An explicit `enabled: false`
|
|
143
|
+
// in the settings store is what stops either one, and stopping it takes effect
|
|
144
|
+
// on the very next tick.
|
|
119
145
|
const { createAutoDrain, createAutoMint } = require('./lib/auto.js');
|
|
120
146
|
|
|
121
147
|
// The shared `~/.aegiscode/.env` loader (client/env-file.js, staged to
|
|
@@ -260,6 +286,12 @@ const MENU_SEARCH_CHANNEL = `${IPC_PREFIX}menuSearch`;
|
|
|
260
286
|
* knows the open session id, main.js just pings it. */
|
|
261
287
|
const MENU_EXPORT_MARKDOWN_CHANNEL = `${IPC_PREFIX}menuExportMarkdown`;
|
|
262
288
|
const MENU_EXPORT_JSON_CHANNEL = `${IPC_PREFIX}menuExportJson`;
|
|
289
|
+
/** View > Authorship panel. The φ(α) card that used to open this sheet was
|
|
290
|
+
* removed from the sidebar (sealing runs in the background now), so the menu
|
|
291
|
+
* is what keeps the panel — key, records, vault — reachable at all. Same split
|
|
292
|
+
* as the channels above: main has no panel state, it just asks the renderer to
|
|
293
|
+
* open the sheet it already owns. */
|
|
294
|
+
const MENU_FINGERPRINT_CHANNEL = `${IPC_PREFIX}menuFingerprint`;
|
|
263
295
|
|
|
264
296
|
/**
|
|
265
297
|
* Main -> renderer push for a resolved aegis:// deep link (D? deep linking).
|
|
@@ -830,7 +862,7 @@ function providerRouteConfigured(engine) {
|
|
|
830
862
|
* reject), injected by bootstrap() so this module still needs no `electron`
|
|
831
863
|
* import to stay unit-testable.
|
|
832
864
|
*/
|
|
833
|
-
function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured, avatar) {
|
|
865
|
+
function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured, avatar, getVaultBackend) {
|
|
834
866
|
// One service for the window's lifetime, bound to the app's data dir — but
|
|
835
867
|
// created LAZILY, which is what the comment here always claimed and what the
|
|
836
868
|
// code did not do. `createFingerprintService({ dir })` throws on a missing
|
|
@@ -844,12 +876,12 @@ function createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConf
|
|
|
844
876
|
let fingerprintService = null;
|
|
845
877
|
const fingerprint = new Proxy({}, {
|
|
846
878
|
get(_target, prop) {
|
|
847
|
-
if (!fingerprintService) fingerprintService = createFingerprintService({ dir });
|
|
879
|
+
if (!fingerprintService) fingerprintService = createFingerprintService({ dir, getVaultBackend });
|
|
848
880
|
const value = fingerprintService[prop];
|
|
849
881
|
return typeof value === 'function' ? value.bind(fingerprintService) : value;
|
|
850
882
|
},
|
|
851
883
|
has(_target, prop) {
|
|
852
|
-
if (!fingerprintService) fingerprintService = createFingerprintService({ dir });
|
|
884
|
+
if (!fingerprintService) fingerprintService = createFingerprintService({ dir, getVaultBackend });
|
|
853
885
|
return prop in fingerprintService;
|
|
854
886
|
},
|
|
855
887
|
});
|
|
@@ -1046,8 +1078,8 @@ function withReplyNotify(promise, event, onReplyFinished) {
|
|
|
1046
1078
|
* tests, where the safe no-op default in createIpcDispatch takes over.
|
|
1047
1079
|
* `onReplyFinished` is likewise bootstrap()-only: it fires the native
|
|
1048
1080
|
* "reply ready" notification when the window is unfocused/hidden. */
|
|
1049
|
-
function registerIpc(ipcMain, aegis, dir, persistApiKey, openExternal, onReplyFinished, providerConfigured) {
|
|
1050
|
-
const dispatch = createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured);
|
|
1081
|
+
function registerIpc(ipcMain, aegis, dir, persistApiKey, openExternal, onReplyFinished, providerConfigured, getVaultBackend) {
|
|
1082
|
+
const dispatch = createIpcDispatch(aegis, dir, persistApiKey, openExternal, providerConfigured, undefined, getVaultBackend);
|
|
1051
1083
|
for (const [name, handler] of Object.entries(dispatch)) {
|
|
1052
1084
|
if (name === 'chatCompletion') {
|
|
1053
1085
|
// Streaming render (D2.1): when the renderer asks for stream, SSE deltas
|
|
@@ -1154,6 +1186,29 @@ function registerConfirmModeIpc(ipcMain, dispatch) {
|
|
|
1154
1186
|
return dispatch;
|
|
1155
1187
|
}
|
|
1156
1188
|
|
|
1189
|
+
/**
|
|
1190
|
+
* Vault backend preference: `aegis:getVaultBackend` / `aegis:setVaultBackend`.
|
|
1191
|
+
* Same shape as createConfirmModeDispatch — unit-testable with a stub store —
|
|
1192
|
+
* and registered through the same generic registerConfirmModeIpc, which does
|
|
1193
|
+
* nothing confirm-mode-specific; it just wires `{name: handler}` pairs as
|
|
1194
|
+
* `aegis:<name>`.
|
|
1195
|
+
*
|
|
1196
|
+
* `getVaultBackend` answers `{ backend }`, one of
|
|
1197
|
+
* desktop/lib/settings.js's VAULT_BACKEND_VALUES ('aegiscloud' | 'drive' |
|
|
1198
|
+
* 'off'). `setVaultBackend` refuses an unrecognised value the same way
|
|
1199
|
+
* settings.setVaultBackendConfig does (returns the unchanged state rather than
|
|
1200
|
+
* writing something the fingerprint service's vaultScope()/vaultDisabled()
|
|
1201
|
+
* would not recognise either) — a bad payload cannot silently point the vault
|
|
1202
|
+
* somewhere, or at nothing, that the picker never actually offered.
|
|
1203
|
+
*/
|
|
1204
|
+
function createVaultBackendDispatch(settings) {
|
|
1205
|
+
if (!settings) throw new Error('createVaultBackendDispatch requires a settings store');
|
|
1206
|
+
return {
|
|
1207
|
+
getVaultBackend: () => settings.vaultBackendConfig(),
|
|
1208
|
+
setVaultBackend: (payload) => settings.setVaultBackendConfig(payload && payload.backend),
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1157
1212
|
/**
|
|
1158
1213
|
* Funnel instrumentation (Phase 2 of docs/upgrade-conversion-plan.md):
|
|
1159
1214
|
* `aegis:telemetryEvent` / `aegis:telemetryReport`, backed by the store in
|
|
@@ -2741,6 +2796,10 @@ function createNpmUpdateManager({
|
|
|
2741
2796
|
fetchImpl,
|
|
2742
2797
|
setTimeoutImpl = setTimeout,
|
|
2743
2798
|
clearTimeoutImpl = clearTimeout,
|
|
2799
|
+
// Both injectable so the install path is testable without running npm or
|
|
2800
|
+
// killing the test runner's own process.
|
|
2801
|
+
runUpgrade,
|
|
2802
|
+
relaunch,
|
|
2744
2803
|
} = {}) {
|
|
2745
2804
|
const PKG = 'aegis-desktop';
|
|
2746
2805
|
let current = 'unknown';
|
|
@@ -2762,6 +2821,11 @@ function createNpmUpdateManager({
|
|
|
2762
2821
|
attempts: 0,
|
|
2763
2822
|
retryInMs: 0,
|
|
2764
2823
|
command: null,
|
|
2824
|
+
// `plan` is the shared verdict the banner was rendered from. `download()`
|
|
2825
|
+
// installs from THIS object rather than re-deriving a command, so the
|
|
2826
|
+
// button can only ever run what the banner promised.
|
|
2827
|
+
plan: null,
|
|
2828
|
+
method: null,
|
|
2765
2829
|
canDownload: false,
|
|
2766
2830
|
};
|
|
2767
2831
|
const set = (patch) => {
|
|
@@ -2788,6 +2852,34 @@ function createNpmUpdateManager({
|
|
|
2788
2852
|
let inFlight = null;
|
|
2789
2853
|
let interval = null;
|
|
2790
2854
|
|
|
2855
|
+
/**
|
|
2856
|
+
* How THIS desktop copy was installed: 'npm' | 'pip' | 'npx' | 'source'.
|
|
2857
|
+
*
|
|
2858
|
+
* Memoised — it cannot change while the process runs. `entry` is the app
|
|
2859
|
+
* directory: an unpackaged run is either a global npm install (a path with
|
|
2860
|
+
* `node_modules` in it), a checkout (`desktop/` beside `.git`), or a pip
|
|
2861
|
+
* launcher, which declares itself through AEGIS_INSTALL_METHOD because
|
|
2862
|
+
* nothing in the filesystem would otherwise say so.
|
|
2863
|
+
*/
|
|
2864
|
+
let methodCache = null;
|
|
2865
|
+
function detectMethod() {
|
|
2866
|
+
if (methodCache) return methodCache;
|
|
2867
|
+
const detect = updater && updater.detectInstallMethod;
|
|
2868
|
+
if (typeof detect !== 'function') { methodCache = 'npm'; return methodCache; }
|
|
2869
|
+
try {
|
|
2870
|
+
methodCache = detect({
|
|
2871
|
+
env: process.env,
|
|
2872
|
+
argv: process.argv,
|
|
2873
|
+
pkg: PKG,
|
|
2874
|
+
entry: __dirname,
|
|
2875
|
+
fileExists: (rel) => fs.existsSync(path.resolve(__dirname, '..', rel)),
|
|
2876
|
+
});
|
|
2877
|
+
} catch {
|
|
2878
|
+
methodCache = 'npm';
|
|
2879
|
+
}
|
|
2880
|
+
return methodCache;
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2791
2883
|
/** 1m, 5m, 15m, 60m, 60m… — capped, never unbounded. */
|
|
2792
2884
|
const delayFor = () =>
|
|
2793
2885
|
UPDATE_RETRY_BACKOFF_MS[Math.min(Math.max(attempts - 1, 0), UPDATE_RETRY_BACKOFF_MS.length - 1)];
|
|
@@ -2887,8 +2979,14 @@ function createNpmUpdateManager({
|
|
|
2887
2979
|
// The npm applier's instruction comes from the ONE shared verdict
|
|
2888
2980
|
// (`updatePlan`), the same call `aegiscode`'s notice and `aegis doctor`
|
|
2889
2981
|
// render — so the banner cannot offer a command the terminal would not.
|
|
2982
|
+
//
|
|
2983
|
+
// `method` is this copy's install channel, detected the same way the
|
|
2984
|
+
// CLI does. It matters because `npm i -g` against a pip-installed
|
|
2985
|
+
// aegis-desktop writes a second copy beside the one the user actually
|
|
2986
|
+
// runs: the button would appear to work and change nothing.
|
|
2987
|
+
const method = detectMethod();
|
|
2890
2988
|
const plan = typeof updater.updatePlan === 'function'
|
|
2891
|
-
? updater.updatePlan({ current, latest: res.latest, behind: true, channel: 'npm', pkg: PKG })
|
|
2989
|
+
? updater.updatePlan({ current, latest: res.latest, behind: true, channel: 'npm', pkg: PKG, method })
|
|
2892
2990
|
: null;
|
|
2893
2991
|
return set({
|
|
2894
2992
|
status: 'available',
|
|
@@ -2898,11 +2996,13 @@ function createNpmUpdateManager({
|
|
|
2898
2996
|
transient: false,
|
|
2899
2997
|
attempts: 0,
|
|
2900
2998
|
retryInMs: 0,
|
|
2999
|
+
plan,
|
|
3000
|
+
method,
|
|
2901
3001
|
command: (plan && plan.instruction) || `npm i -g ${PKG}@latest`,
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
2904
|
-
//
|
|
2905
|
-
canDownload:
|
|
3002
|
+
// A channel with an `apply` argv has something this process can run;
|
|
3003
|
+
// `npx` and a source checkout do not, and offering a button that
|
|
3004
|
+
// silently does nothing is worse than the command alone.
|
|
3005
|
+
canDownload: Boolean(plan && plan.apply),
|
|
2906
3006
|
});
|
|
2907
3007
|
}
|
|
2908
3008
|
|
|
@@ -2944,16 +3044,97 @@ function createNpmUpdateManager({
|
|
|
2944
3044
|
}
|
|
2945
3045
|
}
|
|
2946
3046
|
|
|
3047
|
+
/**
|
|
3048
|
+
* Run one installer argv and resolve to a status patch. Never rejects.
|
|
3049
|
+
*
|
|
3050
|
+
* `npm i -g` against a global prefix needs no elevation the way a system
|
|
3051
|
+
* package does, but it still writes outside the app dir and it can fail on a
|
|
3052
|
+
* permissions wall — so a failure is reported with npm's own last lines
|
|
3053
|
+
* rather than a blanket "update failed".
|
|
3054
|
+
*/
|
|
3055
|
+
function runInstall(target) {
|
|
3056
|
+
const exec = runUpgrade || ((cmd, args) => new Promise((resolve) => {
|
|
3057
|
+
execFile(cmd, args, { timeout: 180_000, windowsHide: true }, (err, stdout, stderr) => {
|
|
3058
|
+
if (err) return resolve({ ok: false, output: String(stderr || stdout || err.message || err) });
|
|
3059
|
+
resolve({ ok: true, output: String(stdout || '') });
|
|
3060
|
+
});
|
|
3061
|
+
}));
|
|
3062
|
+
return Promise.resolve(exec(target.command, target.args)).then(
|
|
3063
|
+
(res) => ({ ok: Boolean(res && res.ok), output: String((res && res.output) || '') }),
|
|
3064
|
+
(err) => ({ ok: false, output: errorText(err) }),
|
|
3065
|
+
);
|
|
3066
|
+
}
|
|
3067
|
+
|
|
2947
3068
|
return {
|
|
2948
3069
|
status: () => state,
|
|
2949
3070
|
check,
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
3071
|
+
/**
|
|
3072
|
+
* The banner's Download button. It INSTALLS.
|
|
3073
|
+
*
|
|
3074
|
+
* This used to be a no-op that re-set `command` and `canDownload:false`:
|
|
3075
|
+
* the button was hidden for every npm install, so the affordance and the
|
|
3076
|
+
* action were both missing, and the only way to update the window was to
|
|
3077
|
+
* read a shell command off a banner and type it into a different app.
|
|
3078
|
+
*
|
|
3079
|
+
* It still runs only on an explicit click (nothing here installs on a
|
|
3080
|
+
* timer), and it still does not hot-swap the running process: after a
|
|
3081
|
+
* successful install the honest state is 'downloaded' — "restart to install"
|
|
3082
|
+
* — because the new files are on disk and this process is still the old
|
|
3083
|
+
* ones. That is the same promise the packaged electron-updater path makes,
|
|
3084
|
+
* so the banner needs no branch.
|
|
3085
|
+
*/
|
|
3086
|
+
download: async () => {
|
|
3087
|
+
const target = state.plan && state.plan.apply;
|
|
3088
|
+
if (!target) {
|
|
3089
|
+
// Nothing to install on this channel (npx fetches fresh every run; a
|
|
3090
|
+
// checkout belongs to its git remote). Say who owns it instead of
|
|
3091
|
+
// pretending to act.
|
|
3092
|
+
return set({
|
|
3093
|
+
status: state.status === 'available' ? 'available' : state.status,
|
|
3094
|
+
command: (state.plan && state.plan.instruction) || state.command,
|
|
3095
|
+
canDownload: false,
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
set({ status: 'downloading', error: null, progress: 0 });
|
|
3099
|
+
const res = await runInstall(target);
|
|
3100
|
+
if (!res.ok) {
|
|
3101
|
+
// `transient: false` deliberately: an installer that refused is not a
|
|
3102
|
+
// network blip, and the retry ladder must not quietly re-run an npm
|
|
3103
|
+
// install every minute.
|
|
3104
|
+
return set({
|
|
3105
|
+
status: 'error',
|
|
3106
|
+
transient: false,
|
|
3107
|
+
error: res.output.trim().split('\n').slice(-3).join(' ').trim() || 'the installer failed',
|
|
3108
|
+
command: `${target.command} ${target.args.join(' ')}`,
|
|
3109
|
+
});
|
|
3110
|
+
}
|
|
3111
|
+
return set({
|
|
3112
|
+
status: 'downloaded',
|
|
3113
|
+
version: state.version,
|
|
3114
|
+
error: null,
|
|
3115
|
+
progress: 100,
|
|
3116
|
+
command: `npm i -g ${PKG}@latest`,
|
|
3117
|
+
canDownload: false,
|
|
3118
|
+
});
|
|
3119
|
+
},
|
|
3120
|
+
/**
|
|
3121
|
+
* "Restart to install". Relaunches the app so the freshly installed files
|
|
3122
|
+
* are what runs — `app.relaunch()` before `app.exit()`, the standard pair,
|
|
3123
|
+
* because `app.quit()` alone would not start a new process.
|
|
3124
|
+
*/
|
|
3125
|
+
quitAndInstall: () => {
|
|
3126
|
+
if (state.status !== 'downloaded') return;
|
|
3127
|
+
const again = relaunch || (() => {
|
|
3128
|
+
try {
|
|
3129
|
+
const { app } = require('electron');
|
|
3130
|
+
app.relaunch();
|
|
3131
|
+
app.exit(0);
|
|
3132
|
+
} catch {
|
|
3133
|
+
/* not under Electron (a test or the CLI): nothing to relaunch */
|
|
3134
|
+
}
|
|
3135
|
+
});
|
|
3136
|
+
again();
|
|
3137
|
+
},
|
|
2957
3138
|
start: () => {
|
|
2958
3139
|
check().catch(() => {});
|
|
2959
3140
|
interval = setInterval(() => check().catch(() => {}), UPDATE_CHECK_INTERVAL_MS);
|
|
@@ -3156,6 +3337,10 @@ function buildAppMenu({ app, Menu, BrowserWindow, updateManager }) {
|
|
|
3156
3337
|
label: 'Export Session…',
|
|
3157
3338
|
click: () => sendToFocusedWindow(BrowserWindow, MENU_EXPORT_JSON_CHANNEL),
|
|
3158
3339
|
};
|
|
3340
|
+
const fingerprintItem = {
|
|
3341
|
+
label: 'Authorship Panel…',
|
|
3342
|
+
click: () => sendToFocusedWindow(BrowserWindow, MENU_FINGERPRINT_CHANNEL),
|
|
3343
|
+
};
|
|
3159
3344
|
|
|
3160
3345
|
const template = [
|
|
3161
3346
|
...(isMac
|
|
@@ -3206,6 +3391,8 @@ function buildAppMenu({ app, Menu, BrowserWindow, updateManager }) {
|
|
|
3206
3391
|
submenu: [
|
|
3207
3392
|
searchItem,
|
|
3208
3393
|
{ type: 'separator' },
|
|
3394
|
+
fingerprintItem,
|
|
3395
|
+
{ type: 'separator' },
|
|
3209
3396
|
{ role: 'reload', accelerator: 'CmdOrCtrl+R' },
|
|
3210
3397
|
{ role: 'forceReload' },
|
|
3211
3398
|
{ type: 'separator' },
|
|
@@ -3360,6 +3547,26 @@ function bootstrap() {
|
|
|
3360
3547
|
describeFailure: autoProvision.provisionFailureText,
|
|
3361
3548
|
onChange: (state) => broadcastAccountStatus(state),
|
|
3362
3549
|
});
|
|
3550
|
+
|
|
3551
|
+
// Google sign-in + Drive connect (lib/google-connect.js). Same key store as
|
|
3552
|
+
// accountKey above (settings.setAegisKey) — one saved key, whichever door
|
|
3553
|
+
// last wrote it — and the same banner: a successful Google sign-in clears
|
|
3554
|
+
// whatever "no key" state accountKey was showing, same as a successful
|
|
3555
|
+
// register-device retry does.
|
|
3556
|
+
const googleConnect = createGoogleConnect({
|
|
3557
|
+
aegis,
|
|
3558
|
+
credentials,
|
|
3559
|
+
persistKey: (key) => settings.setAegisKey(key),
|
|
3560
|
+
openUrl: (url) => {
|
|
3561
|
+
if (!isSafeExternalUrl(url)) return false;
|
|
3562
|
+
shell.openExternal(url);
|
|
3563
|
+
return true;
|
|
3564
|
+
},
|
|
3565
|
+
googleSignIn,
|
|
3566
|
+
driveConnect: driveConnectLib,
|
|
3567
|
+
driveUpload: driveUploadLib,
|
|
3568
|
+
onAccountChange: () => broadcastAccountStatus(accountKey.status()),
|
|
3569
|
+
});
|
|
3363
3570
|
const keyPresentAtStart = Boolean(aegis.apiKey);
|
|
3364
3571
|
const earlyKeyProvision = autoProvision.autoProvisionApiKey(aegis, credentials, { app: 'desktop' }).then((result) => {
|
|
3365
3572
|
accountKey.recordStartup(result);
|
|
@@ -3467,7 +3674,8 @@ function bootstrap() {
|
|
|
3467
3674
|
persistApiKey,
|
|
3468
3675
|
(url) => shell.openExternal(url),
|
|
3469
3676
|
handleReplyFinished,
|
|
3470
|
-
() => providerRouteConfigured(engine)
|
|
3677
|
+
() => providerRouteConfigured(engine),
|
|
3678
|
+
() => settings.vaultBackendConfig().backend
|
|
3471
3679
|
);
|
|
3472
3680
|
const modelIpc = registerModelIpc(
|
|
3473
3681
|
ipcMain,
|
|
@@ -3495,6 +3703,15 @@ function bootstrap() {
|
|
|
3495
3703
|
// the CLI, which shares this store.
|
|
3496
3704
|
registerConfirmModeIpc(ipcMain, withAutoRoundsSwitch(createConfirmModeDispatch(settings), modelIpc.modelDispatch));
|
|
3497
3705
|
|
|
3706
|
+
// Vault backend preference (Settings/the authorship panel → Google Drive /
|
|
3707
|
+
// AE / No sync): aegis:getVaultBackend / aegis:setVaultBackend. Registered
|
|
3708
|
+
// here, beside confirm mode, for the same reason — this is where the
|
|
3709
|
+
// settings store lands, and lib/fingerprint.js's vaultScope()/vaultDisabled()
|
|
3710
|
+
// read the live value through the getVaultBackend thunks passed to
|
|
3711
|
+
// registerIpc and createAutoMint above, so a change here reaches the very
|
|
3712
|
+
// next publish/anchor/vault/audit call with no restart.
|
|
3713
|
+
registerConfirmModeIpc(ipcMain, createVaultBackendDispatch(settings));
|
|
3714
|
+
|
|
3498
3715
|
// Funnel instrumentation (Phase 2 of docs/upgrade-conversion-plan.md):
|
|
3499
3716
|
// aegis:telemetryEvent / aegis:telemetryReport, backed by
|
|
3500
3717
|
// lib/local/telemetry.js.
|
|
@@ -3539,12 +3756,12 @@ function bootstrap() {
|
|
|
3539
3756
|
// nobody to answer a card, so the interactive engine is the wrong one to hand
|
|
3540
3757
|
// a worker (see createQueueEngine).
|
|
3541
3758
|
//
|
|
3542
|
-
// Registration alone drains nothing.
|
|
3543
|
-
//
|
|
3544
|
-
//
|
|
3545
|
-
// PERSISTED
|
|
3546
|
-
// lib/auto.js on every tick. There is still no boot
|
|
3547
|
-
// drain-on-idle
|
|
3759
|
+
// Registration alone drains nothing. Two ways a drain starts: the queue
|
|
3760
|
+
// card's Run one / Drain all (the card is hidden now, but the IPC and the
|
|
3761
|
+
// CLI entry point are the same call), or the background worker below — a
|
|
3762
|
+
// PERSISTED setting (settings.js `__autoDrain`), ON unless explicitly
|
|
3763
|
+
// switched off, read by lib/auto.js on every tick. There is still no boot
|
|
3764
|
+
// drain and no drain-on-idle: the first tick is one interval after start.
|
|
3548
3765
|
const queueDispatch = registerQueueIpc(ipcMain, {
|
|
3549
3766
|
engine: createQueueEngine(aegis, settings),
|
|
3550
3767
|
aegis,
|
|
@@ -3584,7 +3801,12 @@ function bootstrap() {
|
|
|
3584
3801
|
let autoFingerprint = null;
|
|
3585
3802
|
autoMint = createAutoMint({
|
|
3586
3803
|
mint: (payload) => {
|
|
3587
|
-
if (!autoFingerprint)
|
|
3804
|
+
if (!autoFingerprint) {
|
|
3805
|
+
autoFingerprint = createFingerprintService({
|
|
3806
|
+
dir: dataDir,
|
|
3807
|
+
getVaultBackend: () => settings.vaultBackendConfig().backend,
|
|
3808
|
+
});
|
|
3809
|
+
}
|
|
3588
3810
|
return autoFingerprint.mint(payload || {});
|
|
3589
3811
|
},
|
|
3590
3812
|
settings,
|
|
@@ -3592,10 +3814,12 @@ function bootstrap() {
|
|
|
3592
3814
|
|
|
3593
3815
|
registerAutoIpc(ipcMain, createAutoDispatch({ autoDrain, autoMint }));
|
|
3594
3816
|
|
|
3595
|
-
// Arm
|
|
3596
|
-
//
|
|
3597
|
-
//
|
|
3598
|
-
//
|
|
3817
|
+
// Arm the background workers. Both start() calls are reads of a persisted
|
|
3818
|
+
// setting that is ON unless it was explicitly switched off (settings.js
|
|
3819
|
+
// `__autoDrain`/`__autoMint`), so a default install drains a queued task with
|
|
3820
|
+
// no click and seals a quiet session with nothing on screen. Neither start()
|
|
3821
|
+
// drains or seals anything by itself: the drain's first tick is one interval
|
|
3822
|
+
// away and the mint waits for a finished turn before its quiet clock starts.
|
|
3599
3823
|
autoDrain.start();
|
|
3600
3824
|
autoMint.start();
|
|
3601
3825
|
|
|
@@ -3820,6 +4044,14 @@ function bootstrap() {
|
|
|
3820
4044
|
registerUpdateIpc(ipcMain, updateManager);
|
|
3821
4045
|
ipcMain.handle(`${IPC_PREFIX}accountStatus`, () => accountKey.status());
|
|
3822
4046
|
ipcMain.handle(`${IPC_PREFIX}registerAccount`, () => accountKey.registerAccount());
|
|
4047
|
+
// Google sign-in + Drive connect/sync (lib/google-connect.js). Every call
|
|
4048
|
+
// resolves `{ ok, … }` / `{ ok: false, error }` rather than throwing, same
|
|
4049
|
+
// contract as the fingerprint* handlers below — the renderer shows the
|
|
4050
|
+
// actual reason instead of Electron's opaque "Error invoking remote method".
|
|
4051
|
+
ipcMain.handle(`${IPC_PREFIX}driveSignIn`, () => googleConnect.signInWithGoogle());
|
|
4052
|
+
ipcMain.handle(`${IPC_PREFIX}driveConnect`, () => googleConnect.connectDrive());
|
|
4053
|
+
ipcMain.handle(`${IPC_PREFIX}driveStatus`, () => googleConnect.status());
|
|
4054
|
+
ipcMain.handle(`${IPC_PREFIX}driveSyncMemory`, () => googleConnect.syncMemoryNow());
|
|
3823
4055
|
registerExportIpc(
|
|
3824
4056
|
ipcMain,
|
|
3825
4057
|
sessionStore,
|
|
@@ -4104,6 +4336,7 @@ module.exports = {
|
|
|
4104
4336
|
MENU_SEARCH_CHANNEL,
|
|
4105
4337
|
MENU_EXPORT_MARKDOWN_CHANNEL,
|
|
4106
4338
|
MENU_EXPORT_JSON_CHANNEL,
|
|
4339
|
+
MENU_FINGERPRINT_CHANNEL,
|
|
4107
4340
|
DEEP_LINK_CHANNEL,
|
|
4108
4341
|
UPDATE_STATUS_CHANNEL,
|
|
4109
4342
|
UPDATE_CHECK_INTERVAL_MS,
|
|
@@ -4139,6 +4372,7 @@ module.exports = {
|
|
|
4139
4372
|
createQuickLauncherDispatch,
|
|
4140
4373
|
registerQuickLauncherIpc,
|
|
4141
4374
|
createConfirmModeDispatch,
|
|
4375
|
+
createVaultBackendDispatch,
|
|
4142
4376
|
withAutoRoundsSwitch,
|
|
4143
4377
|
createTelemetryDispatch,
|
|
4144
4378
|
registerTelemetryIpc,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.13",
|
|
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",
|
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"start": "electron .",
|
|
28
28
|
"predist": "node scripts/predist.mjs",
|
|
29
|
+
"prepack": "node scripts/predist.mjs --quiet",
|
|
29
30
|
"prepublishOnly": "npm run predist",
|
|
30
31
|
"icon": "node scripts/generate-icon.mjs",
|
|
31
32
|
"shots": "node scripts/capture-marketing-shots.mjs",
|
|
32
33
|
"demo": "node scripts/record-demo-gif.mjs",
|
|
33
34
|
"dist": "npm run predist && electron-builder",
|
|
34
35
|
"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 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/account-key.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
|
+
"check": "node --check main.js && node --check preload.js && node --check renderer/app.js && node --check renderer/quick.js && node --check renderer/providers.js && node --check renderer/provider-toolbar.js && node --check renderer/companion.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/account-key.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
37
|
"test:shell": "node ../test/desktop-shell.mjs",
|
|
37
38
|
"test:model": "node ../test/model-dispatch.mjs",
|
|
38
39
|
"test:budget": "node ../test/budget.test.mjs",
|
|
@@ -50,6 +51,8 @@
|
|
|
50
51
|
"test:stream-policy": "node ../test/stream-policy.test.mjs",
|
|
51
52
|
"test:renderer-dom": "node ../test/renderer-dom.test.mjs",
|
|
52
53
|
"test:renderer-wiring": "node ../test/renderer-wiring.test.mjs",
|
|
54
|
+
"test:renderer-welcome": "node test/renderer-welcome.test.mjs",
|
|
55
|
+
"test:renderer-providers": "node ../test/renderer-providers.test.mjs",
|
|
53
56
|
"test:avatar-xp": "node test/avatar-xp.test.mjs",
|
|
54
57
|
"test:avatar-level": "node test/avatar-level.test.mjs",
|
|
55
58
|
"test:avatar-events": "node test/avatar-events.test.mjs",
|
package/preload.js
CHANGED
|
@@ -32,6 +32,7 @@ const MENU_NEW_CHAT_CHANNEL = `${IPC_PREFIX}menuNewChat`;
|
|
|
32
32
|
const MENU_SEARCH_CHANNEL = `${IPC_PREFIX}menuSearch`;
|
|
33
33
|
const MENU_EXPORT_MARKDOWN_CHANNEL = `${IPC_PREFIX}menuExportMarkdown`;
|
|
34
34
|
const MENU_EXPORT_JSON_CHANNEL = `${IPC_PREFIX}menuExportJson`;
|
|
35
|
+
const MENU_FINGERPRINT_CHANNEL = `${IPC_PREFIX}menuFingerprint`;
|
|
35
36
|
const DEEP_LINK_CHANNEL = `${IPC_PREFIX}deepLink`;
|
|
36
37
|
const QUICK_LAUNCHER_PUSH_CHANNEL = `${IPC_PREFIX}quickLauncherPush`;
|
|
37
38
|
|
|
@@ -204,6 +205,14 @@ const api = {
|
|
|
204
205
|
getConfirmMode: () => invoke('getConfirmMode'),
|
|
205
206
|
setConfirmMode: (enabled) => invoke('setConfirmMode', { enabled: Boolean(enabled) }),
|
|
206
207
|
|
|
208
|
+
// Vault backend (the authorship panel's Google Drive / AE / No sync picker):
|
|
209
|
+
// main.js createVaultBackendDispatch persists it in the settings store's
|
|
210
|
+
// reserved `__vaultBackend` namespace, and lib/fingerprint.js's
|
|
211
|
+
// vaultScope()/vaultDisabled() read it on every publish/anchor/vault/audit
|
|
212
|
+
// call. Both resolve `{ backend }`.
|
|
213
|
+
getVaultBackend: () => invoke('getVaultBackend'),
|
|
214
|
+
setVaultBackend: (backend) => invoke('setVaultBackend', { backend: String(backend || '') }),
|
|
215
|
+
|
|
207
216
|
// Funnel instrumentation (Phase 2 of docs/upgrade-conversion-plan.md). The
|
|
208
217
|
// renderer emits exactly two event names, on two surfaces, carrying a
|
|
209
218
|
// campaign and a surface — nothing else, ever. This method picks those three
|
|
@@ -276,6 +285,20 @@ const api = {
|
|
|
276
285
|
ipcRenderer.on(ACCOUNT_STATUS_CHANNEL, listener);
|
|
277
286
|
return () => ipcRenderer.removeListener(ACCOUNT_STATUS_CHANNEL, listener);
|
|
278
287
|
},
|
|
288
|
+
// Google sign-in + Drive connect/sync (main.js lib/google-connect.js).
|
|
289
|
+
// `driveSignIn` opens a browser for aegis1's `/auth/google` round-trip and
|
|
290
|
+
// replaces this machine's saved key with the Google-linked one — the
|
|
291
|
+
// desktop counterpart of the CLI's `aegiscode connect`. `driveConnect`
|
|
292
|
+
// opens `/connect/drive` and polls until this (now Google-linked) account
|
|
293
|
+
// shows a Drive token on file; it is only coherent once `driveSignIn` has
|
|
294
|
+
// actually run. `driveStatus` is a one-shot, no-browser read for painting
|
|
295
|
+
// the card on load. `driveSyncMemory` pulls this account's AEGIS cloud
|
|
296
|
+
// memory and backs it up to a dedicated "AEGIS Memory" Drive folder. Every
|
|
297
|
+
// one resolves `{ ok, … }` / `{ ok: false, error }`, never throws.
|
|
298
|
+
driveSignIn: () => invoke('driveSignIn'),
|
|
299
|
+
driveConnect: () => invoke('driveConnect'),
|
|
300
|
+
driveStatus: () => invoke('driveStatus'),
|
|
301
|
+
driveSyncMemory: () => invoke('driveSyncMemory'),
|
|
279
302
|
// Native menu accelerators with no built-in Electron role (Cmd/Ctrl+N,
|
|
280
303
|
// Cmd/Ctrl+K — see main.js buildAppMenu): main.js just pings the channel,
|
|
281
304
|
// the renderer owns what "new chat" / "search" actually do.
|
|
@@ -298,6 +321,14 @@ const api = {
|
|
|
298
321
|
ipcRenderer.on(MENU_EXPORT_MARKDOWN_CHANNEL, listener);
|
|
299
322
|
return () => ipcRenderer.removeListener(MENU_EXPORT_MARKDOWN_CHANNEL, listener);
|
|
300
323
|
},
|
|
324
|
+
// View > Authorship Panel… — the sidebar card that used to open the φ(α)
|
|
325
|
+
// sheet is gone (sealing runs in the background), so the menu is the entry
|
|
326
|
+
// point. The renderer owns the sheet, main only asks for it.
|
|
327
|
+
onMenuFingerprint: (onTrigger) => {
|
|
328
|
+
const listener = () => onTrigger();
|
|
329
|
+
ipcRenderer.on(MENU_FINGERPRINT_CHANNEL, listener);
|
|
330
|
+
return () => ipcRenderer.removeListener(MENU_FINGERPRINT_CHANNEL, listener);
|
|
331
|
+
},
|
|
301
332
|
onMenuExportJson: (onTrigger) => {
|
|
302
333
|
const listener = () => onTrigger();
|
|
303
334
|
ipcRenderer.on(MENU_EXPORT_JSON_CHANNEL, listener);
|
|
@@ -452,16 +483,17 @@ const quickLauncher = {
|
|
|
452
483
|
// task's own tool loop runs in — and main.js refuses it unless it is an
|
|
453
484
|
// absolute path that already exists; this file never joins, reads or writes
|
|
454
485
|
// a path itself.
|
|
455
|
-
// 2. No drain that starts by itself
|
|
456
|
-
// `drain`/`proceed` above are
|
|
457
|
-
//
|
|
458
|
-
//
|
|
459
|
-
//
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
486
|
+
// 2. No drain that starts by itself unless the persisted switch says so —
|
|
487
|
+
// and in a default install it DOES say so. `drain`/`proceed` above are the
|
|
488
|
+
// manual entry points (the queue card is hidden now; the same calls back
|
|
489
|
+
// the `aegis autonomous` CLI path), and the timer that calls `proceed()`
|
|
490
|
+
// with no click lives in desktop/lib/auto.js, armed from settings.js's
|
|
491
|
+
// reserved `__autoDrain` namespace. That namespace is ON unless an
|
|
492
|
+
// explicit `enabled: false` is written — which is exactly what
|
|
493
|
+
// `aegis.auto.setDrain({ enabled: false })` below persists. Every tick
|
|
494
|
+
// re-reads it, so switching it off stops the very next one. Draining
|
|
495
|
+
// spends money on a real model in a real checkout, so the switch stays in
|
|
496
|
+
// the settings store rather than in this file's defaults.
|
|
465
497
|
const queue = {
|
|
466
498
|
// Queue one task. `payload` is { task, cwd, class, model, effort, workers,
|
|
467
499
|
// maxRounds, commit }; main.js range-checks every field and answers
|
|
@@ -514,8 +546,9 @@ const queue = {
|
|
|
514
546
|
* app's own scope (client/attest.js), so no key can be handed in from the DOM
|
|
515
547
|
* even by a mistake in main.js.
|
|
516
548
|
*
|
|
517
|
-
* Both switches are
|
|
518
|
-
*
|
|
549
|
+
* Both switches are ON unless the settings store says otherwise; these calls
|
|
550
|
+
* are how a host turns one OFF. Nothing on screen has to ask first — see the
|
|
551
|
+
* queue section's note 2 above.
|
|
519
552
|
*/
|
|
520
553
|
const auto = {
|
|
521
554
|
// { ok, drain: { enabled, intervalMs, armed, busy, ticks, drains, last, lastSkip }, mint: { … } }
|