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/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 opt-in automations (lib/auto.js): the auto-drain timer and the
113
- // seal-on-quiet auto-mint. Both are DEFAULT OFF and read their switch from the
114
- // settings store's reserved `__autoDrain`/`__autoMint` namespaces on every tick,
115
- // so this require wires the TIMING only — the queue worker still owns the lock
116
- // and lib/fingerprint.js still owns the seal. See the header of lib/auto.js for
117
- // why the "nothing drains unless the user asked" rule became a default rather
118
- // than an absolute.
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
- // npm owns installation here, so there is nothing for this process
2903
- // to download — the banner must offer the command, not a Download
2904
- // button that would silently do nothing.
2905
- canDownload: false,
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
- // npm installs are the user's to run; saying so beats pretending to act.
2951
- download: async () => set({
2952
- status: state.status === 'available' ? 'available' : state.status,
2953
- command: `npm i -g ${PKG}@latest`,
2954
- canDownload: false,
2955
- }),
2956
- quitAndInstall: () => {},
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. There are now exactly two ways a drain
3543
- // starts, and both are visible in the UI: a click on the card's
3544
- // Run one / Drain all, or the card's "drain automatically" switch — a
3545
- // PERSISTED, DEFAULT-OFF setting (settings.js `__autoDrain`) read by
3546
- // lib/auto.js on every tick. There is still no boot drain and no
3547
- // drain-on-idle; the interval only exists while the switch is on.
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) autoFingerprint = createFingerprintService({ dir: dataDir });
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 whatever the user left switched on — and nothing else. Both start()
3596
- // calls are reads of a persisted, default-off setting: on a default install
3597
- // the drain timer is not created at all and the mint has no pending turns, so
3598
- // the queue still waits for a click and no session is ever sealed by itself.
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.10",
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 UNLESS the persisted switch is armed.
456
- // `drain`/`proceed` above are invoked only from the queue card's two
457
- // buttons; the timer that can call one without a click lives in
458
- // desktop/lib/auto.js and is armed by exactly one thing — the
459
- // `aegis.auto.setDrain({ enabled: true })` call the card's toggle makes
460
- // below, which writes settings.js's reserved `__autoDrain` namespace. It
461
- // is OFF in a default install, it is re-read on every tick, and switching
462
- // it off stops the very next one. Draining spends money on a real model in
463
- // a real checkout, so that switch is persisted and visible rather than
464
- // implied — but it is also not hidden from this file's users any more.
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 OFF until one of these calls says otherwise, and the only
518
- * caller is a toggle the user clicked — see the queue section's note 2 above.
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: { … } }