aegis-desktop 0.7.6 → 0.7.8

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.
@@ -45,6 +45,10 @@ const os = require('node:os');
45
45
 
46
46
  const toolsModule = require('./tools.js');
47
47
  const promptModule = require('./prompt.js');
48
+ // The direct-dial policy: which base URLs may reach a provider transport, and
49
+ // why a remote one is refused rather than metered. The same module the CLI
50
+ // resolves (cli/src/custommodels.js), so both hosts answer with one rule.
51
+ const { isLocalEndpoint, remoteRefusal } = require('./endpoints.js');
48
52
  const { ShellSession } = require('./shell.js');
49
53
  const { agentSystemPrompt, agentRoleLabel } = require('./agents.js');
50
54
  // Cooperative working-tree sharing (see each module's header). The lock
@@ -87,8 +91,28 @@ const CLASSES = [
87
91
  { class: 'ollama', label: 'Ollama (local)', kind: 'local' },
88
92
  { class: 'openai-compat', label: 'Custom OpenAI-compatible', kind: 'custom' },
89
93
  { class: 'anthropic', label: 'Anthropic-compatible', kind: 'custom' },
94
+ { class: 'byok', label: 'Bring your own key', kind: 'cloud' },
90
95
  ];
91
96
 
97
+ /** Local settings namespace for one BYOK provider's key. A `byok:` prefix
98
+ * keeps this out of the 'anthropic'/'openai-compat' CUSTOM_CLASSES' own
99
+ * namespaces, which are a different feature (a self-hosted/compatible
100
+ * endpoint's base URL + key) — same store, deliberately separate rows. */
101
+ function byokNamespace(providerId) {
102
+ return `byok:${providerId}`;
103
+ }
104
+
105
+ /** Split a byok model id ("anthropic:claude-sonnet-5") into its provider and
106
+ * bare model parts. `modelId` may itself contain ':' (none of today's
107
+ * catalog ids do, but nothing guarantees that), so only the FIRST segment is
108
+ * the provider — the rest re-joins as the model. */
109
+ function splitByokModel(compound) {
110
+ const s = typeof compound === 'string' ? compound : '';
111
+ const i = s.indexOf(':');
112
+ if (i < 0) return { provider: '', model: s };
113
+ return { provider: s.slice(0, i), model: s.slice(i + 1) };
114
+ }
115
+
92
116
  /**
93
117
  * Mirrors aegiscodex-dev's src/backend.js DEEPSEEK_REASONING_MODEL_RE +
94
118
  * EFFORT_TOKEN_BUDGET verbatim. DeepSeek's reasoning models (deepseek-flash,
@@ -637,14 +661,26 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
637
661
  * a base URL is mandatory for both, and Anthropic additionally needs its own
638
662
  * key (the wire format authenticates with x-api-key). Reporting them as
639
663
  * always-ready made chat() POST to `${undefined}/v1/…` (defect #2).
664
+ *
665
+ * A stored REMOTE base URL is reported as not-configured rather than ready,
666
+ * plus `blocked` and the reason. This is the third face of the direct-dial
667
+ * gate (storage in settings.js set, dispatch in chat below): a row that
668
+ * predates the rule must not be OFFERED either, or the class list advertises
669
+ * a lane the turn then refuses. The reason travels so the UI can explain it
670
+ * instead of showing a dead row — and it is recoverable by design: saving a
671
+ * local URL (or clearing the field) makes the class usable again, which is
672
+ * what the refusal text tells the user to do.
640
673
  */
641
674
  function customStatus(cls) {
642
675
  const cfg = settings.get(cls) || {};
643
676
  const baseURL = typeof cfg.baseURL === 'string' ? cfg.baseURL.trim() : '';
644
677
  const hasBase = Boolean(baseURL);
645
678
  const hasKey = Boolean(cfg.configured);
679
+ const blocked = hasBase && !isLocalEndpoint(baseURL);
646
680
  return {
647
- configured: cls === 'anthropic' ? hasBase && hasKey : hasBase,
681
+ configured: !blocked && (cls === 'anthropic' ? hasBase && hasKey : hasBase),
682
+ blocked,
683
+ ...(blocked ? { blockedReason: remoteRefusal(baseURL, { subject: `the ${cls} endpoint` }) } : {}),
648
684
  baseURL,
649
685
  keyMask: cfg.keyMask || null,
650
686
  };
@@ -657,6 +693,16 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
657
693
  if (c.class === 'aegis') {
658
694
  return { ...c, configured: Boolean(aegis.apiKey) };
659
695
  }
696
+ if (c.class === 'byok') {
697
+ // Configured means "at least one provider has a locally-stored key",
698
+ // not a single baseURL+key pair like the CUSTOM_CLASSES below — byok
699
+ // holds one row per provider (byokNamespace), so customStatus's shape
700
+ // does not apply here.
701
+ const anyConfigured = (settings.list() || []).some(
702
+ (s) => s && typeof s.provider === 'string' && s.provider.startsWith('byok:') && s.configured
703
+ );
704
+ return { ...c, configured: anyConfigured };
705
+ }
660
706
  return { ...c, ...customStatus(c.class) };
661
707
  });
662
708
  }
@@ -681,6 +727,56 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
681
727
  const tags = await ollama.listTags();
682
728
  return { class: cls, models: tags.map((t) => ({ id: t.id })) };
683
729
  }
730
+ if (cls === 'byok') {
731
+ // The server's catalog names every provider it accepts a key for, the
732
+ // models each unlocks, and whether an AEGIS account key is even needed
733
+ // to ask (it is not — the catalog answers an anonymous request).
734
+ // Deliberately NOT gated on aegis.apiKey the way the pooled class above
735
+ // is: BYOK's whole point is a caller who brings their own credential, and
736
+ // hiding the catalog would hide the answer to "which key do I go and get"
737
+ // from exactly the person deciding whether to bother. Nothing here
738
+ // enforces billing either: the desktop is one client of a route that is
739
+ // unauthenticated by design, so refusing in this process would stop
740
+ // exactly one host out of many. The relay owns that decision, and reports
741
+ // it via `fee.require_balance` below.
742
+ let providers = [];
743
+ let fee = null;
744
+ try {
745
+ const data = await aegis.byokProviders();
746
+ providers = (data && data.providers) || [];
747
+ // The handling fee AEGIS adds on top of the caller's vendor bill. It is
748
+ // the server's own published rate (services/pricing.price_byok_call) and
749
+ // is passed through untouched — never re-derived here, because a client
750
+ // that hardcodes a fee is a client that can disagree with the ledger.
751
+ // Absent until the server publishes one, and the UI must then say
752
+ // nothing rather than show a guess.
753
+ fee = (data && data.fee) || null;
754
+ } catch {
755
+ providers = [];
756
+ }
757
+ const models = [];
758
+ for (const p of providers) {
759
+ if (!p || !p.id) continue;
760
+ const local = settings.get(byokNamespace(p.id)) || {};
761
+ const rawModels = Array.isArray(p.models) ? p.models : [];
762
+ for (const m of rawModels) {
763
+ const modelId = typeof m === 'string' ? m : m && m.id;
764
+ if (!modelId) continue;
765
+ models.push({
766
+ id: `${p.id}:${modelId}`,
767
+ label: `${p.label || p.id} — ${modelId}`,
768
+ provider: p.id,
769
+ configured: Boolean(local.configured),
770
+ });
771
+ }
772
+ }
773
+ return {
774
+ class: cls, models, providers,
775
+ needsProviderKey: models.length > 0 && !models.some((m) => m.configured),
776
+ needsAegisKey: !aegis.apiKey,
777
+ fee,
778
+ };
779
+ }
684
780
  // Custom endpoints: the model id is the *user's* choice — a provider model
685
781
  // name, never a URL. Offering the configured base URL as an `id` meant that
686
782
  // leaving the default selection POSTed `model: "https://api.openai.com/v1"`,
@@ -690,7 +786,18 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
690
786
  // along for display only.
691
787
  const cfg = settings.get(cls) || {};
692
788
  const baseURL = typeof cfg.baseURL === 'string' ? cfg.baseURL.trim() : '';
693
- return { class: cls, models: [], needsModelId: true, baseURL };
789
+ // Same reporting as customStatus: a stored remote URL is not a usable
790
+ // model list, and the reason has to reach the UI with it (see customStatus).
791
+ const blocked = Boolean(baseURL) && !isLocalEndpoint(baseURL);
792
+ return {
793
+ class: cls,
794
+ models: [],
795
+ needsModelId: true,
796
+ baseURL,
797
+ ...(blocked
798
+ ? { blocked: true, blockedReason: remoteRefusal(baseURL, { subject: `the ${cls} endpoint` }) }
799
+ : {}),
800
+ };
694
801
  }
695
802
 
696
803
  /**
@@ -867,6 +974,53 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
867
974
  });
868
975
  }
869
976
 
977
+ if (cls === 'byok') {
978
+ // opts.model is still the compound "provider:model" id here — the
979
+ // relay wants them split (a `provider` field plus a bare `model`).
980
+ // opts.apiKey is the PROVIDER key resolved by chat() above (from the
981
+ // byokNamespace(provider) settings row), never the account's own AEGIS
982
+ // key — that one is baked into the `aegis` client already and attached
983
+ // automatically by byokChatCompletion() as X-AEGIS-Key so the account
984
+ // gets billed the handling fee; see client/aegis.js.
985
+ const { provider, model: bareModel } = splitByokModel(opts.model);
986
+ try {
987
+ return await aegis.byokChatCompletion({
988
+ provider,
989
+ model: bareModel,
990
+ providerKey: opts.apiKey,
991
+ prompt: opts.prompt,
992
+ system: opts.system,
993
+ messages: opts.messages,
994
+ maxTokens: opts.maxTokens,
995
+ stream: true,
996
+ onStream: opts.onDelta,
997
+ signal: opts.signal,
998
+ });
999
+ } catch (e) {
1000
+ // The relay's own balance gate (aegis1 `AEGIS_BYOK_REQUIRE_BALANCE`,
1001
+ // app.py `byok_chat_completions`) answers 402 with a body aimed at an
1002
+ // API consumer — "Insufficient balance. Top up to continue using
1003
+ // BYOK." Shown raw in a chat transcript that reads as a crash rather
1004
+ // than a bill, so the one thing the user has to DO is said here, in the
1005
+ // hosts' own voice. The status is preserved so every existing caller
1006
+ // (the CLI's error painter, the desktop turn guard) still sees a 402.
1007
+ //
1008
+ // The provider key is untouched by this: it is the caller's own and it
1009
+ // is still valid. What ran out is the AEGIS balance the handling fee
1010
+ // is billed against, which is the whole reason this lane has a fee.
1011
+ if (e && e.status === 402) {
1012
+ const err = new Error(
1013
+ 'byok: this account has no AEGIS balance left, and the BYOK handling fee is ' +
1014
+ 'billed there — your provider key is still valid, this is not a key problem. ' +
1015
+ 'Top up the account, then try again (or /class aegis to use the pooled lane).'
1016
+ );
1017
+ err.status = 402;
1018
+ throw err;
1019
+ }
1020
+ throw e;
1021
+ }
1022
+ }
1023
+
870
1024
  const common = {
871
1025
  baseURL: opts.cfg.baseURL,
872
1026
  apiKey: opts.apiKey,
@@ -887,7 +1041,14 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
887
1041
  async function chat(payload, onDelta) {
888
1042
  const cls = payload && payload.class;
889
1043
  const model = payload && payload.model;
890
- const maxTokens = reasoningBudget(cls, model, payload && payload.maxTokens, payload && payload.effort);
1044
+ // byok's model id is "provider:model" (see splitByokModel) — strip the
1045
+ // provider prefix before pattern-matching, or a DeepSeek reasoning model
1046
+ // selected under byok ("byok" model "deepseek:deepseek-v4-flash") never
1047
+ // matches DEEPSEEK_REASONING_MODEL_RE's anchored pattern and silently
1048
+ // gets no stated budget, which is exactly the "hidden CoT ate the whole
1049
+ // default and returned nothing" failure this budget exists to prevent.
1050
+ const reasoningModelId = cls === 'byok' ? splitByokModel(model).model : model;
1051
+ const maxTokens = reasoningBudget(cls, reasoningModelId, payload && payload.maxTokens, payload && payload.effort);
891
1052
  // The caller's OWN number, kept apart from `maxTokens` above. That one
892
1053
  // collapses two different facts into a single value — "the caller stated
893
1054
  // 4096" and "effort implies 32768" — and the pooled path must treat them
@@ -937,8 +1098,12 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
937
1098
  const signal = controller.signal;
938
1099
 
939
1100
  // A caller can opt out of the agent loop entirely (`tools: false`) and get
940
- // the old single-shot turn back.
941
- const toolsEnabled = !(payload && payload.tools === false);
1101
+ // the old single-shot turn back. byok is ALWAYS single-shot: the
1102
+ // stateless relay (aegis.byokChatCompletion, /api/v1/byok/chat/completions)
1103
+ // has no tools/tool_choice parameter at all, so sending schemas here would
1104
+ // build a prompt promising tool access the transport silently drops —
1105
+ // the model would reason about exec/readFile and never see a result.
1106
+ const toolsEnabled = cls !== 'byok' && !(payload && payload.tools === false);
942
1107
  const wire = cls === 'anthropic' ? 'anthropic' : 'openai';
943
1108
  const toolSchemas = toolsEnabled ? T.toolsFor(wire, { includeSubagent: depth < MAX_SUBAGENT_DEPTH }) : [];
944
1109
  const toolChoice = (payload && payload.toolChoice) || null;
@@ -965,8 +1130,89 @@ function createLocalEngine({ aegis, settings, ollama, providers, tools, promptBu
965
1130
  const turnCwd = envFor(payload).cwd;
966
1131
 
967
1132
  try {
968
- const cfg = cls === 'aegis' || cls === 'ollama' ? {} : settings.get(cls) || {};
969
- const apiKey = cls === 'aegis' || cls === 'ollama' ? null : settings.rawKey(cls);
1133
+ // byok's settings row lives under the provider named IN THE MODEL id
1134
+ // ("anthropic:claude-sonnet-5" -> byokNamespace('anthropic')), never
1135
+ // under the literal class name — one flat 'byok' row could not hold
1136
+ // more than one provider's key at a time.
1137
+ const byokParts = cls === 'byok' ? splitByokModel(model) : null;
1138
+ const cfg =
1139
+ cls === 'aegis' || cls === 'ollama' ? {}
1140
+ : cls === 'byok' ? settings.get(byokNamespace(byokParts.provider)) || {}
1141
+ : settings.get(cls) || {};
1142
+ const apiKey =
1143
+ cls === 'aegis' || cls === 'ollama' ? null
1144
+ : cls === 'byok' ? settings.rawKey(byokNamespace(byokParts.provider))
1145
+ : settings.rawKey(cls);
1146
+
1147
+ if (cls === 'byok' && (!byokParts.provider || !byokParts.model)) {
1148
+ const err = new Error(
1149
+ "byok: model must be \"<provider>:<model>\" (pick one from /models) — got " +
1150
+ JSON.stringify(model || '')
1151
+ );
1152
+ err.status = 400;
1153
+ throw err;
1154
+ }
1155
+ if (cls === 'byok' && !apiKey) {
1156
+ const err = new Error(
1157
+ `byok: no key saved for "${byokParts.provider}" — add one before chatting with this model.`
1158
+ );
1159
+ err.status = 400;
1160
+ throw err;
1161
+ }
1162
+ // …and the AEGIS account key is what makes the turn BILLABLE at all. The
1163
+ // relay authenticates on the provider key and resolves the payer
1164
+ // separately, from X-AEGIS-Key (app.py `_byok_identify_user`): with no
1165
+ // account key the server can attribute the handling fee to no one, so it
1166
+ // is logged against user 0 as uncollected — an anonymous free ride the
1167
+ // fee exists to close. Require the account key here so EVERY BYOK turn is
1168
+ // attributed and every caller pays the handling fee: a funded balance is
1169
+ // debited immediately, an unfunded one records the fee as owed
1170
+ // (token_bank.charge_byok clamps to zero and never refuses), and nobody
1171
+ // is served free. Balance is the server's own concern, not this gate's —
1172
+ // with a balance or without one, the account still pays. `listModels`
1173
+ // still answers anonymously (the catalog is how a user finds out which
1174
+ // key to get), so this refuses only the send, and only until a key lands.
1175
+ if (cls === 'byok' && !aegis.apiKey) {
1176
+ const err = new Error(
1177
+ 'byok: connect your AEGIS account key first — the BYOK handling fee is billed there. ' +
1178
+ 'Add it in the Status card (or run /login), then try again.'
1179
+ );
1180
+ err.status = 401;
1181
+ throw err;
1182
+ }
1183
+
1184
+ // THE DIRECT-DIAL GATE. A custom class talks to the user's base URL
1185
+ // itself (providers.js openaiCompatible / anthropicMessages), and that
1186
+ // transport bills NOBODY: no pooled margin, no BYOK handling fee, no
1187
+ // account key attached. The only usage it may therefore carry is an
1188
+ // endpoint on this machine, where there is no vendor to pay. A remote URL
1189
+ // here is an unpaid turn, and it is refused rather than metered because
1190
+ // there is nothing to meter it against — the relay accepts a fixed
1191
+ // catalog of provider ids (services/nexus_provider/catalog.py), so an
1192
+ // arbitrary remote URL cannot be billed there either.
1193
+ //
1194
+ // Checked at DISPATCH and not only at storage (settings.js set refuses
1195
+ // the same URL): a row written before this rule existed, or hand-edited
1196
+ // into settings.json, is still in the file, and reading it back happily
1197
+ // would keep the lane open. Both seams, one rule — the message is
1198
+ // endpoints.js's, so the desktop and the CLI say the same thing.
1199
+ if (CUSTOM_CLASSES.includes(cls)) {
1200
+ const raw = cfg && typeof cfg.baseURL === 'string' ? cfg.baseURL.trim() : '';
1201
+ if (raw && !isLocalEndpoint(raw)) {
1202
+ const err = new Error(
1203
+ remoteRefusal(raw, {
1204
+ subject: `the ${cls} endpoint`,
1205
+ hint:
1206
+ 'This class dials your URL directly and bills nobody, so it is local-only. ' +
1207
+ 'Use a model on this machine, or move the provider to the BYOK card (/class byok), ' +
1208
+ 'which relays through AEGIS and charges the handling fee.',
1209
+ })
1210
+ );
1211
+ err.status = 400;
1212
+ err.code = 'CUSTOM_ENDPOINT_NOT_LOCAL';
1213
+ throw err;
1214
+ }
1215
+ }
970
1216
 
971
1217
  // Custom classes carry no enumerable model list (see listModels), so a
972
1218
  // blank id here means the user never typed one. Fail loudly in-process
package/lib/settings.js CHANGED
@@ -17,6 +17,12 @@
17
17
 
18
18
  const fs = require('node:fs');
19
19
  const path = require('node:path');
20
+ // The direct-dial policy (see that file): which rows may hold a remote base URL.
21
+ // Enforced HERE, at the storage seam, so the unusable configuration cannot be
22
+ // created by either host — the desktop's Settings pane or the CLI's `/model add`
23
+ // — rather than only being refused at send time. Pure node builtins, so this
24
+ // store stays loadable without Electron.
25
+ const { isLocalEndpoint, remoteRefusal, isDirectDialRow } = require('./local/endpoints.js');
20
26
 
21
27
  const SETTINGS_FILE = 'settings.json';
22
28
 
@@ -124,8 +130,13 @@ function createSettingsStore({ dir, safeStorage } = {}) {
124
130
  function save(data) {
125
131
  fs.mkdirSync(path.dirname(file), { recursive: true });
126
132
  const tmp = `${file}.tmp-${process.pid}`;
127
- fs.writeFileSync(tmp, JSON.stringify(data, null, 2));
133
+ // 0600, written before the rename so the file is never briefly readable:
134
+ // this store holds provider keys, and both hosts are now able to write it
135
+ // (the CLI uses the same file, without Electron's safeStorage, so at-rest
136
+ // base64 is all the protection there is — file mode is the real control).
137
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 });
128
138
  fs.renameSync(tmp, file);
139
+ try { fs.chmodSync(file, 0o600); } catch {}
129
140
  }
130
141
 
131
142
  /** Provider CRUD must never touch a reserved (AEGIS-key) namespace. */
@@ -149,8 +160,38 @@ function createSettingsStore({ dir, safeStorage } = {}) {
149
160
  };
150
161
  }
151
162
 
163
+ /**
164
+ * Write a provider row.
165
+ *
166
+ * A direct-dial row ('openai-compat', 'anthropic', 'custom:*') may not hold a
167
+ * remote base URL: those classes talk to the URL themselves (providers.js),
168
+ * which bills nobody, so the only usage they may carry is an endpoint on this
169
+ * machine. Refusing at STORAGE — the earliest seam, shared by the desktop's
170
+ * Settings pane and the CLI — means a remote custom endpoint cannot be
171
+ * configured into existence in the first place; the engine's dispatch gate
172
+ * then covers the rows that predate this rule or were hand-edited into the
173
+ * file. Empty is allowed (that is how the row is cleared, and how a
174
+ * not-yet-configured row looks), and every other namespace is untouched:
175
+ * `byok:<provider>` rows are remote by definition and reach their provider
176
+ * through AEGIS's relay, which is what bills them.
177
+ */
152
178
  function set(provider, { baseURL, key } = {}) {
153
179
  assertNotReserved(provider);
180
+ if (baseURL !== undefined && isDirectDialRow(provider)) {
181
+ const next = String(baseURL == null ? '' : baseURL).trim();
182
+ if (next && !isLocalEndpoint(next)) {
183
+ const err = new Error(remoteRefusal(next, {
184
+ subject: `the ${provider} endpoint`,
185
+ hint:
186
+ 'This lane is for a model running on this machine. To use a remote provider, ' +
187
+ 'save it under Bring-your-own-key (the CLI\'s /class byok), where the AEGIS handling ' +
188
+ 'fee bills the turn.',
189
+ }));
190
+ err.status = 400;
191
+ err.code = 'CUSTOM_ENDPOINT_NOT_LOCAL';
192
+ throw err;
193
+ }
194
+ }
154
195
  const data = load();
155
196
  const cfg = data[provider] || {};
156
197
  if (baseURL !== undefined) cfg.baseURL = baseURL;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aegis-desktop",
3
3
  "productName": "AEGIS Desktop",
4
- "version": "0.7.6",
4
+ "version": "0.7.8",
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",
@@ -29,7 +29,7 @@
29
29
  "icon": "node scripts/generate-icon.mjs",
30
30
  "dist": "npm run predist && electron-builder",
31
31
  "dist:dir": "npm run predist && electron-builder --dir",
32
- "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/context.js && node --check lib/local/providers.js && node --check lib/local/ollama.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 bin/aegis.js",
32
+ "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/preset-fill.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/context.js && node --check lib/local/providers.js && node --check lib/local/ollama.js && node --check lib/local/engine.js && node --check lib/local/endpoints.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 bin/aegis.js",
33
33
  "test:shell": "node ../test/desktop-shell.mjs",
34
34
  "test:model": "node ../test/model-dispatch.mjs",
35
35
  "test:budget": "node ../test/budget.test.mjs",
@@ -43,7 +43,8 @@
43
43
  "test:preview-read": "node ../test/desktop-preview-read.test.mjs",
44
44
  "test:stream-policy": "node ../test/stream-policy.test.mjs",
45
45
  "test:renderer-dom": "node ../test/renderer-dom.test.mjs",
46
- "test:renderer-wiring": "node ../test/renderer-wiring.test.mjs"
46
+ "test:renderer-wiring": "node ../test/renderer-wiring.test.mjs",
47
+ "test:preset-fill": "node ../test/preset-fill.test.mjs"
47
48
  },
48
49
  "dependencies": {
49
50
  "electron": "^33.0.0",