aegis-desktop 0.7.4 → 0.7.6
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/sync/sessions.js +1 -1
- package/main.js +1 -1
- package/package.json +1 -1
- package/renderer/app.js +53 -7
- package/renderer/usage.js +37 -18
- package/vendor/aegis.js +24 -1
- package/vendor/credentials.js +4 -4
package/lib/sync/sessions.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* `sessions.json`, while the terminal host kept `~/.aegiscode/history.jsonl`
|
|
9
9
|
* and the MCP plugin could see neither — three hosts, one account, three
|
|
10
10
|
* disjoint views of the conversation. The implementation now lives in
|
|
11
|
-
* `client/session-store.js`, which is the tree
|
|
11
|
+
* `client/session-store.js`, which is the tree every host already bundles,
|
|
12
12
|
* and every call here forwards to it.
|
|
13
13
|
*
|
|
14
14
|
* Two things that matter are deliberately unchanged:
|
package/main.js
CHANGED
|
@@ -39,7 +39,7 @@ const { createClient } = sharedClient;
|
|
|
39
39
|
// The shared credential store (`client/credentials.js`) — the same 0600 file
|
|
40
40
|
// the terminal host writes with `aegiscode login` and the MCP plugin reads. The
|
|
41
41
|
// app resolves its own account key through it, so signing in once serves all
|
|
42
|
-
//
|
|
42
|
+
// four hosts; see resolveStartupKey() and persistApiKey() below.
|
|
43
43
|
let credentials;
|
|
44
44
|
try {
|
|
45
45
|
credentials = require('../client/credentials.js');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aegis-desktop",
|
|
3
3
|
"productName": "AEGIS Desktop",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.6",
|
|
5
5
|
"description": "Thin Electron host for AEGIS — a local chat UI over the shared client/aegis.js transport. Ships transport + UI only; engine logic stays server-side.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "AEGIS Code",
|
package/renderer/app.js
CHANGED
|
@@ -274,12 +274,36 @@ function foldRoll(sessionId, usage, opts = {}) {
|
|
|
274
274
|
* Paint the topbar meter. Hidden while a session has accounted for nothing —
|
|
275
275
|
* an unused thread must not display a `0 tok` it never measured — and shown
|
|
276
276
|
* the moment a turn is folded in.
|
|
277
|
+
*
|
|
278
|
+
* `live`, when given, previews the in-flight turn on top of the session's
|
|
279
|
+
* already-folded total — text estimated the same way `foldRoll` estimates a
|
|
280
|
+
* turn the wire never reported on — WITHOUT folding it: the preview roll
|
|
281
|
+
* returned by `rollTurn` here is thrown away every frame and `rollsBySession`
|
|
282
|
+
* is never written to, so the real `foldRoll` at completion still starts from
|
|
283
|
+
* the untouched persisted total and cannot double-count this turn. (`turns`/
|
|
284
|
+
* `calls` are left at their default +1 rather than forced to 0 — `fmtRoll`
|
|
285
|
+
* treats a roll with both at 0 as "nothing counted yet" and blanks the line,
|
|
286
|
+
* which hid the preview entirely.) Before this, the meter held the previous
|
|
287
|
+
* turn's total frozen for the whole reply and only jumped at the end, which
|
|
288
|
+
* read as "the counter is dead while the AI works".
|
|
277
289
|
*/
|
|
278
|
-
function renderRollMeter(sessionId) {
|
|
290
|
+
function renderRollMeter(sessionId, live) {
|
|
279
291
|
const el = els.sessionMeter;
|
|
280
292
|
if (!el) return;
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
if (currentSessionId !== sessionId) { el.hidden = true; return; }
|
|
294
|
+
let roll = rollsBySession.get(sessionId || '');
|
|
295
|
+
if (live) {
|
|
296
|
+
roll = rollTurn(roll || emptyRoll(), undefined, {
|
|
297
|
+
prompt: live.prompt,
|
|
298
|
+
reply: live.reply,
|
|
299
|
+
// The thinking trace is billed output too — see `estimatedBuckets`. Left
|
|
300
|
+
// out, this preview measured only the visible answer, so on a reasoning
|
|
301
|
+
// model the meter sat on one number for the whole (longest, priciest)
|
|
302
|
+
// phase of the turn and looked dead while the AI was demonstrably working.
|
|
303
|
+
reasoning: live.reasoning,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const line = roll ? fmtRoll(roll) : '';
|
|
283
307
|
el.textContent = line;
|
|
284
308
|
el.hidden = !line;
|
|
285
309
|
if (line) el.title = 'This session, counted the way the CLI counts it — every turn rolled into one running total';
|
|
@@ -312,6 +336,7 @@ function ledgerFields(usage, model, turn, text) {
|
|
|
312
336
|
calls: text && text.calls,
|
|
313
337
|
prompt: text && text.prompt,
|
|
314
338
|
reply: text && text.reply,
|
|
339
|
+
reasoning: text && text.reasoning,
|
|
315
340
|
});
|
|
316
341
|
const fields = row ? Object.assign({}, row) : {};
|
|
317
342
|
if (model) fields.model = model;
|
|
@@ -3146,6 +3171,10 @@ async function send() {
|
|
|
3146
3171
|
}
|
|
3147
3172
|
const bodyEl = pendingEl.querySelector('.body');
|
|
3148
3173
|
if (bodyEl && bodyEl.textContent !== streamedText) bodyEl.textContent = streamedText;
|
|
3174
|
+
// Live estimate so the topbar meter keeps moving while the reply streams
|
|
3175
|
+
// in, instead of sitting frozen on the previous turn's total until this
|
|
3176
|
+
// one resolves — see renderRollMeter's `live` param.
|
|
3177
|
+
renderRollMeter(sessionId, { prompt, reply: streamedText, reasoning: reasoningText });
|
|
3149
3178
|
stickToBottom();
|
|
3150
3179
|
});
|
|
3151
3180
|
|
|
@@ -3225,7 +3254,7 @@ async function send() {
|
|
|
3225
3254
|
// count is never read as a reported one. Printing nothing here while the
|
|
3226
3255
|
// session total moved was the other half of "the counter looks dead".
|
|
3227
3256
|
else {
|
|
3228
|
-
const est = estimatedBuckets(prompt, text);
|
|
3257
|
+
const est = estimatedBuckets(prompt, text, text === reasoningText ? '' : reasoningText);
|
|
3229
3258
|
if (est) bits.push(`~${est.input + est.output} tokens`);
|
|
3230
3259
|
}
|
|
3231
3260
|
if (turn.cost != null) bits.push(fmtCost(turn.cost, turn.real));
|
|
@@ -3244,6 +3273,10 @@ async function send() {
|
|
|
3244
3273
|
// CLI's `appendHistory` rule and the reason the total moves every turn.
|
|
3245
3274
|
prompt,
|
|
3246
3275
|
reply: text,
|
|
3276
|
+
// The thinking trace is billed output and is not part of `text`, so it is
|
|
3277
|
+
// counted here too — guarded, because the same string must never be
|
|
3278
|
+
// estimated twice if a turn ever collapses the two into one.
|
|
3279
|
+
reasoning: text === reasoningText ? '' : reasoningText,
|
|
3247
3280
|
});
|
|
3248
3281
|
const rollLine = fmtRoll(roll);
|
|
3249
3282
|
if (rollLine) bits.push(`session: ${rollLine}`);
|
|
@@ -3259,6 +3292,10 @@ async function send() {
|
|
|
3259
3292
|
...ledgerFields(data && data.usage, model, turn, {
|
|
3260
3293
|
prompt,
|
|
3261
3294
|
reply: text,
|
|
3295
|
+
// The thinking trace is billed output and is not part of `text`; it
|
|
3296
|
+
// has to persist too, or reopening the window rebuilds a roll short
|
|
3297
|
+
// by the longest part of the turn.
|
|
3298
|
+
reasoning: text === reasoningText ? '' : reasoningText,
|
|
3262
3299
|
calls: data && data.calls,
|
|
3263
3300
|
}),
|
|
3264
3301
|
});
|
|
@@ -3293,11 +3330,16 @@ async function send() {
|
|
|
3293
3330
|
// on this path the figure is the text estimate, marked `est` — and
|
|
3294
3331
|
// never a fabricated zero.
|
|
3295
3332
|
const turn = turnAccounting(undefined, model, {});
|
|
3296
|
-
const roll = foldRoll(sessionId, undefined, {
|
|
3333
|
+
const roll = foldRoll(sessionId, undefined, {
|
|
3334
|
+
model,
|
|
3335
|
+
prompt,
|
|
3336
|
+
reply: text,
|
|
3337
|
+
reasoning: text === reasoningText ? '' : reasoningText,
|
|
3338
|
+
});
|
|
3297
3339
|
const stopBits = ['stopped by you'];
|
|
3298
3340
|
if (turn.tokens != null) stopBits.push(`tokens: ${turn.tokens}`);
|
|
3299
3341
|
else {
|
|
3300
|
-
const est = estimatedBuckets(prompt, text);
|
|
3342
|
+
const est = estimatedBuckets(prompt, text, text === reasoningText ? '' : reasoningText);
|
|
3301
3343
|
if (est) stopBits.push(`~${est.input + est.output} tokens`);
|
|
3302
3344
|
}
|
|
3303
3345
|
const stopRollLine = fmtRoll(roll);
|
|
@@ -3307,7 +3349,11 @@ async function send() {
|
|
|
3307
3349
|
await sync.append(sessionId, {
|
|
3308
3350
|
role: 'assistant',
|
|
3309
3351
|
content: text,
|
|
3310
|
-
...ledgerFields(undefined, model, turn, {
|
|
3352
|
+
...ledgerFields(undefined, model, turn, {
|
|
3353
|
+
prompt,
|
|
3354
|
+
reply: text,
|
|
3355
|
+
reasoning: text === reasoningText ? '' : reasoningText,
|
|
3356
|
+
}),
|
|
3311
3357
|
});
|
|
3312
3358
|
} catch {
|
|
3313
3359
|
/* persistence is non-fatal */
|
package/renderer/usage.js
CHANGED
|
@@ -99,17 +99,29 @@ function estimateTokens(text) {
|
|
|
99
99
|
* genuinely reported nothing (no usage, no prompt, no reply) still lands in
|
|
100
100
|
* `unknown` instead of being handed a fabricated zero.
|
|
101
101
|
*
|
|
102
|
+
* `reasoning` is the extended-thinking trace, and it is OUTPUT: a reasoning
|
|
103
|
+
* model bills its chain of thought as output tokens, which is why the wire's
|
|
104
|
+
* own `output_tokens` already includes it. The desktop renders that trace in
|
|
105
|
+
* its own element (app.js `reasoningText`) rather than in the reply body, so
|
|
106
|
+
* the two streams had to be added back together here — an estimate measured off
|
|
107
|
+
* the visible answer alone sat still for the entire think phase, which on a
|
|
108
|
+
* reasoning model is both the longest and the most expensive part of the turn.
|
|
109
|
+
*
|
|
102
110
|
* @param {string} [prompt]
|
|
103
111
|
* @param {string} [reply]
|
|
112
|
+
* @param {string} [reasoning]
|
|
104
113
|
* @returns {{input: number, output: number, cacheRead: number, cacheWrite: number}|null}
|
|
105
114
|
*/
|
|
106
|
-
function estimatedBuckets(prompt, reply) {
|
|
115
|
+
function estimatedBuckets(prompt, reply, reasoning) {
|
|
107
116
|
const hasPrompt = typeof prompt === 'string' && prompt.length > 0;
|
|
108
|
-
const
|
|
117
|
+
const out =
|
|
118
|
+
(typeof reply === 'string' ? reply : '') +
|
|
119
|
+
(typeof reasoning === 'string' ? reasoning : '');
|
|
120
|
+
const hasReply = out.length > 0;
|
|
109
121
|
if (!hasPrompt && !hasReply) return null;
|
|
110
122
|
return {
|
|
111
123
|
input: estimateTokens(prompt),
|
|
112
|
-
output: estimateTokens(
|
|
124
|
+
output: estimateTokens(out),
|
|
113
125
|
cacheRead: 0,
|
|
114
126
|
cacheWrite: 0,
|
|
115
127
|
real: false,
|
|
@@ -250,6 +262,8 @@ function turnAccounting(usage, model, opts = {}) {
|
|
|
250
262
|
* back is that RUNNING TOTAL. The status bar prints `state.tokens`
|
|
251
263
|
* (renderStatus), and `ctrl+t` prints the tallies in one line
|
|
252
264
|
* (`tokenSummary`: `12,400 tok (10,100 in / 2,300 out) · 4 calls · €0.03`).
|
|
265
|
+
* The tallies are reproduced; that one abbreviation is not — `fmtRoll` explains
|
|
266
|
+
* why it drops the parenthetical rather than carrying the CLI's line verbatim.
|
|
253
267
|
*
|
|
254
268
|
* The desktop counted per turn only. Every meta row was a fresh count that
|
|
255
269
|
* reset at the next call, so "what has this session spent" was answerable only
|
|
@@ -337,7 +351,7 @@ function rollTurn(roll, usage, opts = {}) {
|
|
|
337
351
|
// estimate here is what makes the live roll and the rebuilt roll the same
|
|
338
352
|
// number, and what stops the total standing still on exactly the turns the
|
|
339
353
|
// pool declined to report on — the symptom this fallback exists to remove.
|
|
340
|
-
const est = estimatedBuckets(opts.prompt, opts.reply);
|
|
354
|
+
const est = estimatedBuckets(opts.prompt, opts.reply, opts.reasoning);
|
|
341
355
|
if (!est) {
|
|
342
356
|
// Nothing reported AND no text to estimate from. The single case that
|
|
343
357
|
// stays uncounted: `unknown` names the gap, where a fabricated zero would
|
|
@@ -439,7 +453,10 @@ function ledgerRow(usage, turn, opts = {}) {
|
|
|
439
453
|
if (calls !== undefined) row.calls = calls;
|
|
440
454
|
return row;
|
|
441
455
|
}
|
|
442
|
-
|
|
456
|
+
// `reasoning` is part of the billed output, so it is estimated with the
|
|
457
|
+
// reply — a stored row for a thinking-heavy turn must not persist a count
|
|
458
|
+
// that excludes the longest thing the model wrote.
|
|
459
|
+
const est = estimatedBuckets(opts.prompt, opts.reply, opts.reasoning);
|
|
443
460
|
if (!est) return null;
|
|
444
461
|
const row = { tokens: est };
|
|
445
462
|
if (calls !== undefined) row.calls = calls;
|
|
@@ -471,28 +488,30 @@ function fmtTokens(n) {
|
|
|
471
488
|
* an untouched session adds no noise to a turn's meta line.
|
|
472
489
|
*
|
|
473
490
|
* @param {object} [roll]
|
|
474
|
-
* @returns {string} e.g. `12,400 tok
|
|
491
|
+
* @returns {string} e.g. `12,400 tok · 4 calls · $0.0310` — the CLI's fields in
|
|
492
|
+
* the CLI's order, minus the input/output parenthetical (see below).
|
|
475
493
|
*/
|
|
476
494
|
function fmtRoll(roll) {
|
|
477
495
|
const r = roll || emptyRoll();
|
|
478
496
|
if (!r.turns && !r.calls) return '';
|
|
479
|
-
// The
|
|
480
|
-
//
|
|
481
|
-
//
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
497
|
+
// The fields are `tokenSummary`'s (cli/src/app.js:1413) in its order — tokens,
|
|
498
|
+
// calls, money — with one dropped: the `(10,100 in / 2,300 out)` split. `in`
|
|
499
|
+
// and `out` are a terminal status-line shorthand, legible to someone already
|
|
500
|
+
// reading that status line and to nobody else, which is what an unlabelled
|
|
501
|
+
// abbreviation in a GUI topbar turns into. The total is the figure a reader
|
|
502
|
+
// wants; `r.input`/`r.output` are still folded onto the roll (see `rollTurn`)
|
|
503
|
+
// for any surface that wants to show the split with real labels. The call
|
|
504
|
+
// count stays unconditional, because it is the number that reveals a
|
|
505
|
+
// fan-out, and it is not hidden at 1.
|
|
485
506
|
const bits = [];
|
|
486
507
|
// The token half only when something was actually counted. `rollTurn` counts
|
|
487
508
|
// turns and calls BEFORE it looks at the token count, so a dispatch that
|
|
488
509
|
// reported nothing still reaches here — and printing its empty tally would
|
|
489
|
-
// put `0 tok
|
|
490
|
-
//
|
|
491
|
-
//
|
|
510
|
+
// put `0 tok` on the topbar, a figure the meter never took, which reads as a
|
|
511
|
+
// counter that does not move. The turn count is still stated, because that
|
|
512
|
+
// much is true.
|
|
492
513
|
if (r.tokens > 0) {
|
|
493
|
-
bits.push(
|
|
494
|
-
`${fmtTokens(r.tokens)} tok (${fmtTokens(r.input)} in / ${fmtTokens(r.output)} out)`
|
|
495
|
-
);
|
|
514
|
+
bits.push(`${fmtTokens(r.tokens)} tok`);
|
|
496
515
|
}
|
|
497
516
|
bits.push(`${r.calls} call${r.calls === 1 ? '' : 's'}`);
|
|
498
517
|
// Money is where this line departs from `tokenSummary`, deliberately: that
|
package/vendor/aegis.js
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
* orchestration, routing, or tier logic. All of that lives in the private
|
|
8
8
|
* ae-guix product and behind aegiscloud.org.
|
|
9
9
|
*
|
|
10
|
-
* Runs unchanged under
|
|
10
|
+
* Runs unchanged under four hosts:
|
|
11
11
|
* - mcp/server.js (Claude Code MCP plugin) — CommonJS require
|
|
12
12
|
* - desktop/ (thin Electron shell) — CommonJS require
|
|
13
13
|
* (vendored byte-identical copy at desktop/vendor/aegis.js)
|
|
14
|
+
* - cli/ (aegiscode terminal host) — CommonJS require
|
|
15
|
+
* (vendored byte-identical copy at
|
|
16
|
+
* cli/vendor/client/aegis.js)
|
|
14
17
|
* - aegis-online (browser SPA, vendored copy) — <script> tag →
|
|
15
18
|
* window.AegisClient
|
|
16
19
|
*
|
|
@@ -348,6 +351,12 @@ function createClient(opts = {}) {
|
|
|
348
351
|
* provider key never touches AEGIS storage — it is forwarded straight to the
|
|
349
352
|
* provider for this request only. Mirrors chatCompletion()'s streaming /
|
|
350
353
|
* fallback semantics exactly.
|
|
354
|
+
*
|
|
355
|
+
* The caller's *AEGIS* key is sent alongside the provider key as X-AEGIS-Key.
|
|
356
|
+
* Two credentials, two headers, on purpose: the provider key authenticates the
|
|
357
|
+
* upstream call, the AEGIS key says whose bank pays the handling fee. With no
|
|
358
|
+
* AEGIS key configured the header is omitted and the call stays anonymous —
|
|
359
|
+
* allowed, just unbilled (see _byok_identify_user on the server).
|
|
351
360
|
*/
|
|
352
361
|
async function byokChatCompletion({
|
|
353
362
|
provider = 'openai',
|
|
@@ -373,6 +382,10 @@ function createClient(opts = {}) {
|
|
|
373
382
|
'X-AEGIS-Version': clientVersion,
|
|
374
383
|
'X-Provider-Key': key,
|
|
375
384
|
};
|
|
385
|
+
// Identify the payer. Absent key => anonymous call, which the server
|
|
386
|
+
// accepts but cannot bill; sending it is what turns a BYOK turn into
|
|
387
|
+
// billable traffic instead of a free ride.
|
|
388
|
+
if (apiKey) headers['X-AEGIS-Key'] = apiKey;
|
|
376
389
|
if (!stream || typeof onStream !== 'function') {
|
|
377
390
|
return apiPost('/api/v1/byok/chat/completions', { ...body, stream: false }, headers);
|
|
378
391
|
}
|
|
@@ -683,6 +696,15 @@ function createClient(opts = {}) {
|
|
|
683
696
|
return apiGet('/api/user/api-keys');
|
|
684
697
|
}
|
|
685
698
|
|
|
699
|
+
/** The server's BYOK provider catalog: which providers accept a key, which
|
|
700
|
+
* models each unlocks, where to get the key, and the prefix a valid key
|
|
701
|
+
* starts with. Deliberately NOT key-gated — "which key do I go get, and
|
|
702
|
+
* what will it unlock?" is the question asked *before* an AEGIS key exists.
|
|
703
|
+
* An AEGIS key, if present, only adds the `configured` column. */
|
|
704
|
+
async function byokProviders() {
|
|
705
|
+
return apiGet('/api/v1/byok/providers');
|
|
706
|
+
}
|
|
707
|
+
|
|
686
708
|
async function byokSet(provider, providerApiKey) {
|
|
687
709
|
return apiPost('/api/user/api-keys', {
|
|
688
710
|
provider,
|
|
@@ -809,6 +831,7 @@ function createClient(opts = {}) {
|
|
|
809
831
|
tokenBankTopup,
|
|
810
832
|
billingCheckout,
|
|
811
833
|
byokStatus,
|
|
834
|
+
byokProviders,
|
|
812
835
|
byokSet,
|
|
813
836
|
getMemoryToken,
|
|
814
837
|
memorySearch,
|
package/vendor/credentials.js
CHANGED
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
* credentials.js — the ONE AEGIS account credential store, shared by every
|
|
5
5
|
* host in this repo (terminal `aegiscode`, the MCP plugin, the desktop app).
|
|
6
6
|
*
|
|
7
|
-
* Why it lives in `client/`: this directory is the only tree
|
|
8
|
-
* already
|
|
7
|
+
* Why it lives in `client/`: this directory is the only tree every host
|
|
8
|
+
* already bundles. The MCP plugin ships `mcp/` + `client/` and nothing else, so
|
|
9
9
|
* a reader placed here needs no cross-package dependency — which is what the
|
|
10
10
|
* alternative (the MCP host requiring `cli/src/credentials.js`) would have
|
|
11
11
|
* forced, and why that host used to read `AEGIS_API_KEY` from the environment
|
|
12
|
-
* alone while the CLI could save a key to disk. One login, one file,
|
|
13
|
-
* hosts: that is the point of this module.
|
|
12
|
+
* alone while the CLI could save a key to disk. One login, one file, all
|
|
13
|
+
* four hosts: that is the point of this module.
|
|
14
14
|
*
|
|
15
15
|
* Zero dependencies, no host imports. The data dir is resolved here
|
|
16
16
|
* (`aegisHome()`) rather than imported from the CLI's config.js, so this file
|