aegis-desktop 1.0.0 → 1.0.2

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.
Files changed (96) hide show
  1. package/bin/aegis.js +5 -0
  2. package/lib/auto.js +5 -0
  3. package/lib/avatar/companion.js +5 -0
  4. package/lib/avatar/cosmetics.js +5 -0
  5. package/lib/avatar/events.js +5 -0
  6. package/lib/avatar/holder-turn.js +5 -0
  7. package/lib/avatar/holders.js +5 -0
  8. package/lib/avatar/identity.js +5 -0
  9. package/lib/avatar/level.js +5 -0
  10. package/lib/avatar/mirror.js +5 -0
  11. package/lib/avatar/persona.js +5 -0
  12. package/lib/avatar/profile.js +5 -0
  13. package/lib/avatar/register.js +5 -0
  14. package/lib/avatar/store.js +5 -0
  15. package/lib/avatar/tiers.js +5 -0
  16. package/lib/avatar/transfer.js +5 -0
  17. package/lib/avatar/turn.js +5 -0
  18. package/lib/avatar/voice.js +5 -0
  19. package/lib/avatar/xp.js +5 -0
  20. package/lib/deep-link.js +5 -0
  21. package/lib/fingerprint.js +5 -0
  22. package/lib/local/aegis-seal.mjs +5 -0
  23. package/lib/local/agents.js +5 -0
  24. package/lib/local/autonomous.js +5 -0
  25. package/lib/local/context.js +5 -0
  26. package/lib/local/covenant.js +5 -0
  27. package/lib/local/engine.js +129 -0
  28. package/lib/local/entitlement.js +5 -0
  29. package/lib/local/git-scope.js +5 -0
  30. package/lib/local/local.js +5 -0
  31. package/lib/local/prompt.js +5 -0
  32. package/lib/local/queue.js +5 -0
  33. package/lib/local/session-rounds.js +5 -0
  34. package/lib/local/shell.js +88 -9
  35. package/lib/local/telemetry.js +5 -0
  36. package/lib/local/tools.js +13 -6
  37. package/lib/local/turn-guard.js +5 -0
  38. package/lib/local/worktree-lock.js +5 -0
  39. package/lib/provider-key-mirror.js +5 -0
  40. package/lib/quick-launcher.js +5 -0
  41. package/lib/settings.js +5 -0
  42. package/lib/sync/memory-queue.js +5 -0
  43. package/lib/sync/persist-gate.js +5 -0
  44. package/lib/sync/sessions.js +5 -0
  45. package/lib/window-state.js +5 -0
  46. package/main.js +34 -0
  47. package/package.json +1 -1
  48. package/preload.js +14 -0
  49. package/renderer/app.js +111 -0
  50. package/renderer/avatar/assemble.js +5 -0
  51. package/renderer/avatar/avatar.css +7 -0
  52. package/renderer/avatar/avatar.js +5 -0
  53. package/renderer/avatar/cards.js +5 -0
  54. package/renderer/avatar/hud.js +5 -0
  55. package/renderer/avatar/machine.js +5 -0
  56. package/renderer/avatar/pane.js +5 -0
  57. package/renderer/avatar/parts.js +5 -0
  58. package/renderer/budget.js +5 -0
  59. package/renderer/diff.js +5 -0
  60. package/renderer/diffview.js +5 -0
  61. package/renderer/fingerprint.css +7 -0
  62. package/renderer/fingerprint.js +5 -0
  63. package/renderer/fonts/fonts.css +7 -0
  64. package/renderer/index.html +7 -0
  65. package/renderer/markdown.js +5 -0
  66. package/renderer/quick.css +7 -0
  67. package/renderer/quick.html +7 -0
  68. package/renderer/quick.js +5 -0
  69. package/renderer/stream-policy.js +5 -0
  70. package/renderer/style.css +7 -0
  71. package/renderer/transcript-view.js +5 -0
  72. package/renderer/usage.js +5 -0
  73. package/vendor/aegis.js +5 -0
  74. package/vendor/agent-gate.mjs +23 -0
  75. package/vendor/attest.js +5 -0
  76. package/vendor/authorship-key.js +5 -0
  77. package/vendor/auto-provision.js +5 -0
  78. package/vendor/brain-catalog.js +5 -0
  79. package/vendor/byok-catalog.js +5 -0
  80. package/vendor/credentials.js +5 -0
  81. package/vendor/device-fingerprint.js +5 -0
  82. package/vendor/env-file.js +5 -0
  83. package/vendor/fingerprint.js +5 -0
  84. package/vendor/foreign-memory.js +5 -0
  85. package/vendor/hooks.js +5 -0
  86. package/vendor/model-discovery.js +332 -0
  87. package/vendor/platform.js +5 -0
  88. package/vendor/prefs.js +5 -0
  89. package/vendor/presence-constants.js +5 -0
  90. package/vendor/presence.js +5 -0
  91. package/vendor/record-vault.js +5 -0
  92. package/vendor/session-store.js +5 -0
  93. package/vendor/topup.js +5 -0
  94. package/vendor/touch.js +5 -0
  95. package/vendor/update.js +5 -0
  96. package/vendor/user-models.js +5 -0
package/bin/aegis.js CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
4
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
5
+ // Conveying a modified copy, or running one as a network service, owes the
6
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
2
7
  'use strict';
3
8
 
4
9
  /**
package/lib/auto.js CHANGED
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
package/lib/avatar/xp.js CHANGED
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
package/lib/deep-link.js CHANGED
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
4
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
5
+ // Conveying a modified copy, or running one as a network service, owes the
6
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
2
7
  /**
3
8
  * aegis-seal.mjs — codec for ÆL/1 (AEGIS Encoded Language)
4
9
  *
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -335,6 +340,33 @@ function requireSharedUserModels() {
335
340
  throw last;
336
341
  }
337
342
 
343
+ /**
344
+ * The vendor-list helper (`client/model-discovery.js`) — the module that asks a
345
+ * provider's OWN API which model ids exist, so a desktop user is not asked to
346
+ * recall an id the vendor publishes at a URL. Same resolution rule as the two
347
+ * catalog modules above, for the same reason: the CLI's `/model-discover` reads
348
+ * this one module too, so the terminal and the window cannot answer "what does
349
+ * this key reach" differently.
350
+ *
351
+ * It does no I/O at load time (no key store, no network), so requiring it here
352
+ * costs the window nothing until a user asks for a list.
353
+ */
354
+ function requireSharedModelDiscovery() {
355
+ const candidates = [
356
+ () => require('../../../client/model-discovery.js'),
357
+ () => require('../../vendor/model-discovery.js'),
358
+ ];
359
+ let last = null;
360
+ for (const load of candidates) {
361
+ try {
362
+ return load();
363
+ } catch (err) {
364
+ last = err;
365
+ }
366
+ }
367
+ throw last;
368
+ }
369
+
338
370
  /**
339
371
  * How long a turn waits for the working-tree lock before running anyway.
340
372
  *
@@ -614,6 +646,12 @@ const {
614
646
  addUserModel,
615
647
  removeUserModel,
616
648
  } = requireSharedUserModels();
649
+ const {
650
+ discoverModels: discoverVendorModels,
651
+ canDiscover: canDiscoverVendorModels,
652
+ endpointFor: vendorEndpointFor,
653
+ discoverableProviders,
654
+ } = requireSharedModelDiscovery();
617
655
 
618
656
  // ── Agent-loop helpers ──────────────────────────────────────────────────────
619
657
 
@@ -941,6 +979,12 @@ function createLocalEngine({
941
979
  // caller (desktop/main.js, the CLI's deps.js) wires the local class by
942
980
  // simply existing.
943
981
  localTransport: localT = localTransport,
982
+ // Injectable for tests (and for a host that fronts the network itself): the
983
+ // fetch used ONLY by `discoverModels`, which asks a vendor's list API. Every
984
+ // other request in this file goes through `localT`, so this is deliberately
985
+ // not the transport — discovery is an outbound call to api.<vendor>, not to
986
+ // the AEGIS relay. Undefined in production, which means the global fetch.
987
+ fetchImpl,
944
988
  }) {
945
989
  const controllers = new Map(); // sessionId -> AbortController
946
990
  const T = tools || toolsModule;
@@ -2556,6 +2600,28 @@ function createLocalEngine({
2556
2600
  return localT.unloadAll();
2557
2601
  }
2558
2602
 
2603
+ /**
2604
+ * The vendor key for a provider, resolved in the order the NEXT TURN will
2605
+ * resolve it: the saved `byok:<provider>` row first, then `~/.aegiscode/.env`
2606
+ * (loaded into the environment at host start-up by `client/env-file.js`) and
2607
+ * the shell. Discovery must ask with the key the call will run on — a list
2608
+ * fetched with a different one describes a catalog this machine cannot reach.
2609
+ *
2610
+ * Read-only by construction: it never writes a key, never hands the value to
2611
+ * a renderer and never logs it.
2612
+ */
2613
+ function byokKeyForProviderLookup(providerId) {
2614
+ const id = String(providerId == null ? '' : providerId).trim().toLowerCase();
2615
+ if (!id) return '';
2616
+ try {
2617
+ if (settings && typeof settings.rawKey === 'function') {
2618
+ const stored = settings.rawKey(byokNamespace(id));
2619
+ if (typeof stored === 'string' && stored.trim()) return stored.trim();
2620
+ }
2621
+ } catch { /* a store that throws has no key, and that is a reportable state */ }
2622
+ return providerKeyFromEnv(id);
2623
+ }
2624
+
2559
2625
  return {
2560
2626
  CLASSES,
2561
2627
  listClasses,
@@ -2582,6 +2648,69 @@ function createLocalEngine({
2582
2648
  userModels: () => loadUserModels(),
2583
2649
  addUserModel: (provider, model) => addUserModel(provider, model),
2584
2650
  removeUserModel: (provider, model) => removeUserModel(provider, model),
2651
+ // ── asking the vendor, so the user does not have to remember ids ───────
2652
+ //
2653
+ // `discoverModels(provider)` resolves and returns the ids the provider's own
2654
+ // list API serves RIGHT NOW, which is the fix for the chore that produced
2655
+ // `client/byok-catalog.js`'s hand-copied Anthropic table. It never throws:
2656
+ // the shape is the discovery module's own
2657
+ // `{ ok, provider, ids, others, count }` / `{ ok:false, error }`, and the
2658
+ // renderer prints `error` verbatim. A window that guessed at an id and
2659
+ // silently pinned a 404 is the outcome this exists to prevent.
2660
+ //
2661
+ // The key comes from the SAME chain the next turn will use (the saved
2662
+ // `byok:<provider>` row, then `~/.aegiscode/.env`, then the shell), because
2663
+ // a list fetched with a different key than the call runs on would describe
2664
+ // somebody else's catalog.
2665
+ discoverModels: async (provider) => {
2666
+ const id = String(provider == null ? '' : provider).trim().toLowerCase();
2667
+ const key = byokKeyForProviderLookup(id);
2668
+ const res = await discoverVendorModels(id, { key, fetchImpl });
2669
+ // Say what the caller needs to offer the next step: which of these the
2670
+ // relay itself already lists, and which this machine has declared, so the
2671
+ // UI can mark a row instead of the user diffing two lists by eye.
2672
+ if (!res || !res.ok) return res;
2673
+ const declared = [];
2674
+ try {
2675
+ const map = loadUserModels().map;
2676
+ for (const m of Array.isArray(map[id]) ? map[id] : []) declared.push(m);
2677
+ } catch { /* a store that cannot be read marks nothing */ }
2678
+ return { ...res, declared, endpoint: (vendorEndpointFor(id) || {}).url || '' };
2679
+ },
2680
+ // The bulk write (`/model-add <provider> '*'` and the desktop's "Add all"
2681
+ // button): declare every id in one pass, skipping what is already there and
2682
+ // NAMING what was refused rather than aborting the whole batch on one bad
2683
+ // row. Returns counts, never a bare boolean, so both faces can report
2684
+ // "3 added, 2 already declared" instead of claiming work they did not do.
2685
+ addUserModels: (provider, ids) => {
2686
+ const list = Array.isArray(ids) ? ids : [];
2687
+ const out = { provider, added: 0, already: [], failed: [], capped: 0, file: '' };
2688
+ for (const raw of list) {
2689
+ try {
2690
+ const res = addUserModel(provider, raw);
2691
+ out.file = res.file || out.file;
2692
+ if (res.added) out.added += 1;
2693
+ else out.already.push(res.model);
2694
+ } catch (err) {
2695
+ const message = (err && err.message) || String(err);
2696
+ // The store's per-provider ceiling ends the batch as a CEILING. A
2697
+ // vendor serves a hundred ids and the store holds 32, so reporting
2698
+ // that as sixty-eight refused models would describe one deliberate
2699
+ // limit as sixty-eight broken ids.
2700
+ if (/the cap/.test(message)) {
2701
+ out.capped = list.length - out.added - out.already.length - out.failed.length;
2702
+ break;
2703
+ }
2704
+ out.failed.push({ id: String(raw), reason: message });
2705
+ }
2706
+ }
2707
+ return out;
2708
+ },
2709
+ // What the Settings row uses to decide whether to OFFER a list at all: a
2710
+ // provider this client cannot ask (or has no key for) falls back to the
2711
+ // free-form input, which is the path that always works.
2712
+ canDiscoverModels: (provider) => canDiscoverVendorModels(provider),
2713
+ discoverableProviders: () => discoverableProviders(),
2585
2714
  };
2586
2715
  }
2587
2716
 
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -1,3 +1,8 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (C) 2026 Niklas Borneklint — AEGIS Code (aegiscode) / Aegiscloud.
3
+ // AEGIS Code (aegiscode) — source: https://github.com/aegisinfo/aegiscode-plugin
4
+ // Conveying a modified copy, or running one as a network service, owes the
5
+ // corresponding source under this licence (§5/§13) — see LICENSING.md.
1
6
  'use strict';
2
7
 
3
8
  /**
@@ -280,10 +285,34 @@ const SHELL_CACHE = new Map();
280
285
  function resolveSessionShell(options = {}) {
281
286
  const o = shellSeams(options);
282
287
  if (o.platform === 'win32') {
283
- const spec = platform.shellSpec({ platform: 'win32', env: o.env });
288
+ const cmdSpec = platform.shellSpec({ platform: 'win32', env: o.env });
284
289
  const powershell =
285
290
  platform.resolveExecutable('powershell', { platform: 'win32', env: o.env, exists: o.exists }) ||
286
291
  'powershell.exe';
292
+ // ONE-SHOT NOW SPEAKS THE SESSION'S DIALECT TOO (2026-02-14 report, item 1).
293
+ //
294
+ // This used to be `oneShot: cmdSpec` — PowerShell for the session, cmd.exe
295
+ // for every one-shot — and that mismatch is what made `exec` silently wrong
296
+ // mid-turn. When the live session died (sentinel timeout, killed child, a
297
+ // rejected stdin write) the degrade paths below run one-shot, so from that
298
+ // moment on every remaining command in the turn was parsed by cmd.exe while
299
+ // the model kept emitting PowerShell quoting. cmd.exe answers
300
+ // `'"Write-Output \"' is not recognized as an internal or external command`
301
+ // — localized, so on sv-SE it reads `Felaktig syntax för filnamn, katalognamn
302
+ // eller volymetikett`, which looks like a filesystem fault and not a shell
303
+ // change — and nothing in the tool result said the shell had switched.
304
+ //
305
+ // `-Command` accepts exactly the quoting the session accepts, so a
306
+ // degradation is now a transport change and not a language change. cmd.exe
307
+ // is no longer reachable from this path at all: if PowerShell cannot be
308
+ // resolved the spawn fails loudly (oneShotShell reports `error: …`) instead
309
+ // of quietly running the command in a shell that cannot read it.
310
+ const oneShot = {
311
+ cmd: powershell,
312
+ arg: '-Command',
313
+ kind: 'powershell',
314
+ source: 'powershell.exe',
315
+ };
287
316
  return {
288
317
  ok: true,
289
318
  cmd: powershell,
@@ -294,11 +323,16 @@ function resolveSessionShell(options = {}) {
294
323
  unsupported: null,
295
324
  caveat: null,
296
325
  error: null,
297
- // One-shot commands on Windows stay cmd.exe: that is what the pre-session
298
- // `exec` always used, and PowerShell is only the session's dialect.
299
- oneShot: spec,
326
+ oneShot,
300
327
  executed: [],
301
- tried: [{ candidate: powershell, ok: true, reason: 'the Windows default (PowerShell session, cmd.exe one-shot)' }],
328
+ tried: [
329
+ { candidate: powershell, ok: true, reason: 'the Windows default (PowerShell session AND one-shot)' },
330
+ {
331
+ candidate: cmdSpec.cmd,
332
+ ok: false,
333
+ reason: 'cmd.exe is never used for one-shot: it cannot parse the PowerShell quoting the session teaches',
334
+ },
335
+ ],
302
336
  };
303
337
  }
304
338
 
@@ -363,8 +397,11 @@ function oneShotShellSpec(options = {}) {
363
397
  return { ok: false, cmd: null, arg: null, kind: null, error: spec.error, spec };
364
398
  }
365
399
  if (spec.kind === 'powershell') {
366
- const cmdSpec = platform.shellSpec({ platform: 'win32', env: shellSeams(options).env });
367
- return { ok: true, cmd: cmdSpec.cmd, arg: cmdSpec.arg, kind: 'cmd', error: null, spec };
400
+ // The session's own one-shot spec (PowerShell `-Command`), so the `exec`
401
+ // tool's no-session path and this module's fallback cannot disagree about
402
+ // the dialect — which is exactly how cmd.exe used to sneak back in.
403
+ const shot = spec.oneShot || platform.shellSpec({ platform: 'win32', env: shellSeams(options).env });
404
+ return { ok: true, cmd: shot.cmd, arg: shot.arg, kind: shot.kind || 'powershell', error: null, spec };
368
405
  }
369
406
  return { ok: true, cmd: spec.cmd, arg: spec.arg, kind: 'posix-sh', error: null, spec };
370
407
  }
@@ -412,6 +449,10 @@ class ShellSession {
412
449
  this.buf = '';
413
450
  this.alive = false;
414
451
  this._pending = null; // resolver-scan for the in-flight command
452
+ this.cwd = cwd || process.cwd();
453
+ this.disposed = false; // an explicit dispose() must NOT be self-healed
454
+ this._respawns = 0; // one self-heal attempt per session, never a loop
455
+ this._degradedNoted = false; // the degrade marker is emitted once per session
415
456
  this.shellOptions = shellOptions || {};
416
457
  // Resolved BEFORE anything is spawned or written: `shell.kind` is what the
417
458
  // command framing below branches on, so an unverified `$SHELL` can never
@@ -485,7 +526,21 @@ class ShellSession {
485
526
  if (!this.alive && this.shellError) {
486
527
  return Promise.resolve({ content: this.shellError, isError: true });
487
528
  }
488
- return oneShotShell({ command, timeout, working_directory, shellOptions: this.shellOptions });
529
+ // SELF-HEAL BEFORE DEGRADING. The old path latched: once the live session
530
+ // died (a timeout kill, a child exit, a spawn error) every remaining
531
+ // command in the turn ran one-shot, and on Windows that one-shot used to
532
+ // be cmd.exe — so the agent stayed silently wrong for the rest of the
533
+ // turn. Re-probe and re-spawn ONCE instead, and only then fall through.
534
+ if (!this.alive && !this.disposed && this._respawns < 1) {
535
+ this._respawns += 1;
536
+ this.buf = '';
537
+ this.shell = resolveSessionShell(this.shellOptions);
538
+ this.shellError = this.shell.ok ? null : this.shell.error;
539
+ this._start(this.cwd);
540
+ if (this.alive) return this.run(command, { timeout, working_directory });
541
+ }
542
+ return oneShotShell({ command, timeout, working_directory, shellOptions: this.shellOptions })
543
+ .then((result) => this._noteDegraded(result, !this.alive ? 'the persistent session is not running' : 'no usable session dialect'));
489
544
  }
490
545
  let cmd = String(command || '');
491
546
 
@@ -557,7 +612,8 @@ class ShellSession {
557
612
  this.child.stdin.write(script);
558
613
  } catch {
559
614
  this.alive = false;
560
- oneShotShell({ command, timeout, working_directory, shellOptions: this.shellOptions }).then(finish);
615
+ oneShotShell({ command, timeout, working_directory, shellOptions: this.shellOptions })
616
+ .then((r) => finish(this._noteDegraded(r, 'the session stdin rejected the command')));
561
617
  return;
562
618
  }
563
619
  // Data may already be buffered (fast commands) — scan once now.
@@ -565,7 +621,30 @@ class ShellSession {
565
621
  });
566
622
  }
567
623
 
624
+ /**
625
+ * Mark a result that came from the one-shot fallback rather than the live
626
+ * session, so the model is TOLD the shell changed and can stop trusting its
627
+ * quoting contract.
628
+ *
629
+ * The 2026-02-14 report's worst finding was precisely that this happened
630
+ * silently: "nothing in the result tells the agent the shell changed" while
631
+ * the agent "degrades to a one-shot cmd.exe — and stays there for the
632
+ * remainder of the turn", so it kept emitting PowerShell quoting, got
633
+ * localized cmd.exe syntax errors, and finally blamed the user's machine.
634
+ * Emitted ONCE per session so a degraded turn stays readable.
635
+ */
636
+ _noteDegraded(result, reason) {
637
+ const r = result && typeof result === 'object' ? result : { content: String(result == null ? '' : result), isError: true };
638
+ if (this._degradedNoted) return r;
639
+ this._degradedNoted = true;
640
+ const via = this.shell && this.shell.cmd ? this.shell.cmd : 'unknown shell';
641
+ const was = this.shell && this.shell.kind ? this.shell.kind : 'none';
642
+ const marker = `[shell_degraded] ran one-shot via ${via} (${reason}); session dialect was ${was} — quoting contracts may differ`;
643
+ return { ...r, content: `${marker}\n${r.content || ''}` };
644
+ }
645
+
568
646
  dispose() {
647
+ this.disposed = true;
569
648
  this.alive = false;
570
649
  if (this.child) {
571
650
  try { this.child.stdin.end(); } catch { /* already gone */ }