@atomic-chat/core 0.20.7 → 0.21.0

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 (216) hide show
  1. package/dist/cli/launch.d.ts +23 -3
  2. package/dist/cli/launch.js +59 -8
  3. package/dist/cli/launch.js.map +1 -1
  4. package/dist/cli/main.d.ts +1 -1
  5. package/dist/contracts/events.d.ts +8 -0
  6. package/dist/contracts/settings.d.ts +5 -0
  7. package/dist/contracts/settings.js.map +1 -1
  8. package/dist/core/atomic-core.d.ts +3 -1
  9. package/dist/core/atomic-core.js +4 -1
  10. package/dist/core/atomic-core.js.map +1 -1
  11. package/dist/core/create.js +4 -1
  12. package/dist/core/create.js.map +1 -1
  13. package/dist/core/sessions.js +1 -0
  14. package/dist/core/sessions.js.map +1 -1
  15. package/dist/integrations/catalog.d.ts +25 -8
  16. package/dist/integrations/catalog.js +105 -38
  17. package/dist/integrations/catalog.js.map +1 -1
  18. package/dist/integrations/configure/atomic-agent.d.ts +9 -1
  19. package/dist/integrations/configure/atomic-agent.js +42 -3
  20. package/dist/integrations/configure/atomic-agent.js.map +1 -1
  21. package/dist/integrations/configure/backup.d.ts +21 -0
  22. package/dist/integrations/configure/backup.js +52 -0
  23. package/dist/integrations/configure/backup.js.map +1 -0
  24. package/dist/integrations/configure/claude-code.d.ts +11 -4
  25. package/dist/integrations/configure/claude-code.js +73 -24
  26. package/dist/integrations/configure/claude-code.js.map +1 -1
  27. package/dist/integrations/configure/cline.d.ts +14 -4
  28. package/dist/integrations/configure/cline.js +78 -6
  29. package/dist/integrations/configure/cline.js.map +1 -1
  30. package/dist/integrations/configure/codex.d.ts +5 -3
  31. package/dist/integrations/configure/codex.js +12 -5
  32. package/dist/integrations/configure/codex.js.map +1 -1
  33. package/dist/integrations/configure/copilot.d.ts +28 -5
  34. package/dist/integrations/configure/copilot.js +124 -8
  35. package/dist/integrations/configure/copilot.js.map +1 -1
  36. package/dist/integrations/configure/droid.d.ts +13 -7
  37. package/dist/integrations/configure/droid.js +48 -17
  38. package/dist/integrations/configure/droid.js.map +1 -1
  39. package/dist/integrations/configure/dsh.d.ts +36 -20
  40. package/dist/integrations/configure/dsh.js +250 -48
  41. package/dist/integrations/configure/dsh.js.map +1 -1
  42. package/dist/integrations/configure/goose.d.ts +21 -7
  43. package/dist/integrations/configure/goose.js +141 -10
  44. package/dist/integrations/configure/goose.js.map +1 -1
  45. package/dist/integrations/configure/hermes.d.ts +63 -10
  46. package/dist/integrations/configure/hermes.js +360 -75
  47. package/dist/integrations/configure/hermes.js.map +1 -1
  48. package/dist/integrations/configure/json-backup.d.ts +73 -0
  49. package/dist/integrations/configure/json-backup.js +292 -0
  50. package/dist/integrations/configure/json-backup.js.map +1 -0
  51. package/dist/integrations/configure/json-key-backup.d.ts +43 -0
  52. package/dist/integrations/configure/json-key-backup.js +113 -0
  53. package/dist/integrations/configure/json-key-backup.js.map +1 -0
  54. package/dist/integrations/configure/kilo.d.ts +4 -4
  55. package/dist/integrations/configure/kilo.js +6 -9
  56. package/dist/integrations/configure/kilo.js.map +1 -1
  57. package/dist/integrations/configure/legacy-env-block.d.ts +18 -0
  58. package/dist/integrations/configure/legacy-env-block.js +47 -0
  59. package/dist/integrations/configure/legacy-env-block.js.map +1 -0
  60. package/dist/integrations/configure/mimo.d.ts +3 -2
  61. package/dist/integrations/configure/mimo.js +8 -9
  62. package/dist/integrations/configure/mimo.js.map +1 -1
  63. package/dist/integrations/configure/muse.d.ts +3 -3
  64. package/dist/integrations/configure/muse.js +5 -6
  65. package/dist/integrations/configure/muse.js.map +1 -1
  66. package/dist/integrations/configure/openclaude.d.ts +22 -7
  67. package/dist/integrations/configure/openclaude.js +132 -39
  68. package/dist/integrations/configure/openclaude.js.map +1 -1
  69. package/dist/integrations/configure/openclaw.d.ts +19 -5
  70. package/dist/integrations/configure/openclaw.js +79 -23
  71. package/dist/integrations/configure/openclaw.js.map +1 -1
  72. package/dist/integrations/configure/opencode-style.d.ts +53 -11
  73. package/dist/integrations/configure/opencode-style.js +218 -23
  74. package/dist/integrations/configure/opencode-style.js.map +1 -1
  75. package/dist/integrations/configure/opencode.d.ts +5 -4
  76. package/dist/integrations/configure/opencode.js +8 -10
  77. package/dist/integrations/configure/opencode.js.map +1 -1
  78. package/dist/integrations/configure/openhands.d.ts +5 -4
  79. package/dist/integrations/configure/openhands.js +7 -7
  80. package/dist/integrations/configure/openhands.js.map +1 -1
  81. package/dist/integrations/configure/pi.d.ts +6 -5
  82. package/dist/integrations/configure/pi.js +79 -30
  83. package/dist/integrations/configure/pi.js.map +1 -1
  84. package/dist/integrations/configure/poolside.d.ts +5 -4
  85. package/dist/integrations/configure/poolside.js +7 -7
  86. package/dist/integrations/configure/poolside.js.map +1 -1
  87. package/dist/integrations/configure/registry.d.ts +6 -0
  88. package/dist/integrations/configure/registry.js +1 -0
  89. package/dist/integrations/configure/registry.js.map +1 -1
  90. package/dist/integrations/configure/zcode.d.ts +6 -7
  91. package/dist/integrations/configure/zcode.js +26 -11
  92. package/dist/integrations/configure/zcode.js.map +1 -1
  93. package/dist/integrations/configure/zed.d.ts +16 -4
  94. package/dist/integrations/configure/zed.js +61 -13
  95. package/dist/integrations/configure/zed.js.map +1 -1
  96. package/dist/models/gguf/kv-cache.d.ts +20 -1
  97. package/dist/models/gguf/kv-cache.js +97 -7
  98. package/dist/models/gguf/kv-cache.js.map +1 -1
  99. package/dist/models/gguf/reader.d.ts +5 -3
  100. package/dist/models/gguf/reader.js +10 -6
  101. package/dist/models/gguf/reader.js.map +1 -1
  102. package/dist/runtime/llamacpp/args.d.ts +14 -0
  103. package/dist/runtime/llamacpp/args.js +48 -4
  104. package/dist/runtime/llamacpp/args.js.map +1 -1
  105. package/dist/runtime/llamacpp/ctx-ladder.d.ts +6 -3
  106. package/dist/runtime/llamacpp/ctx-ladder.js +6 -3
  107. package/dist/runtime/llamacpp/ctx-ladder.js.map +1 -1
  108. package/dist/runtime/llamacpp/fit-margin.d.ts +24 -0
  109. package/dist/runtime/llamacpp/fit-margin.js +31 -0
  110. package/dist/runtime/llamacpp/fit-margin.js.map +1 -1
  111. package/dist/runtime/llamacpp/generation-limits.d.ts +45 -0
  112. package/dist/runtime/llamacpp/generation-limits.js +61 -0
  113. package/dist/runtime/llamacpp/generation-limits.js.map +1 -0
  114. package/dist/runtime/llamacpp/load-plan.d.ts +14 -2
  115. package/dist/runtime/llamacpp/load-plan.js +64 -13
  116. package/dist/runtime/llamacpp/load-plan.js.map +1 -1
  117. package/dist/runtime/llamacpp/runtime.d.ts +1 -0
  118. package/dist/runtime/llamacpp/runtime.js +2 -0
  119. package/dist/runtime/llamacpp/runtime.js.map +1 -1
  120. package/dist/runtime/managed-engines/runtime.js +13 -2
  121. package/dist/runtime/managed-engines/runtime.js.map +1 -1
  122. package/dist/runtime/managed-engines/spec.d.ts +6 -0
  123. package/dist/runtime/managed-engines/spec.js.map +1 -1
  124. package/dist/runtime/managed-models/auto-limits.d.ts +47 -0
  125. package/dist/runtime/managed-models/auto-limits.js +60 -0
  126. package/dist/runtime/managed-models/auto-limits.js.map +1 -0
  127. package/dist/runtime/managed-models/index.d.ts +1 -0
  128. package/dist/runtime/managed-models/index.js +1 -0
  129. package/dist/runtime/managed-models/index.js.map +1 -1
  130. package/dist/runtime/mlx/config.d.ts +9 -5
  131. package/dist/runtime/mlx/config.js +12 -6
  132. package/dist/runtime/mlx/config.js.map +1 -1
  133. package/dist/runtime/mlx/model-files.d.ts +9 -0
  134. package/dist/runtime/mlx/model-files.js +26 -0
  135. package/dist/runtime/mlx/model-files.js.map +1 -1
  136. package/dist/runtime/mlx/runtime.d.ts +7 -0
  137. package/dist/runtime/mlx/runtime.js +24 -2
  138. package/dist/runtime/mlx/runtime.js.map +1 -1
  139. package/dist/runtime/sglang/adapter.d.ts +4 -0
  140. package/dist/runtime/sglang/adapter.js +9 -6
  141. package/dist/runtime/sglang/adapter.js.map +1 -1
  142. package/dist/runtime/sglang/engine.js +2 -1
  143. package/dist/runtime/sglang/engine.js.map +1 -1
  144. package/dist/runtime/sglang/memory.d.ts +11 -1
  145. package/dist/runtime/sglang/memory.js +34 -3
  146. package/dist/runtime/sglang/memory.js.map +1 -1
  147. package/dist/runtime/sglang/settings.d.ts +2 -1
  148. package/dist/runtime/sglang/settings.js +11 -7
  149. package/dist/runtime/sglang/settings.js.map +1 -1
  150. package/dist/runtime/vllm/adapter.d.ts +4 -0
  151. package/dist/runtime/vllm/adapter.js +9 -6
  152. package/dist/runtime/vllm/adapter.js.map +1 -1
  153. package/dist/runtime/vllm/engine.js +1 -0
  154. package/dist/runtime/vllm/engine.js.map +1 -1
  155. package/dist/runtime/vllm/memory.d.ts +12 -1
  156. package/dist/runtime/vllm/memory.js +44 -2
  157. package/dist/runtime/vllm/memory.js.map +1 -1
  158. package/dist/runtime/vllm/settings.d.ts +2 -1
  159. package/dist/runtime/vllm/settings.js +12 -7
  160. package/dist/runtime/vllm/settings.js.map +1 -1
  161. package/dist/server/public/context-window.d.ts +29 -0
  162. package/dist/server/public/context-window.js +77 -0
  163. package/dist/server/public/context-window.js.map +1 -0
  164. package/dist/server/public/decision-preview.js +1 -0
  165. package/dist/server/public/decision-preview.js.map +1 -1
  166. package/dist/server/public/errors.d.ts +15 -4
  167. package/dist/server/public/errors.js +53 -7
  168. package/dist/server/public/errors.js.map +1 -1
  169. package/dist/server/public/forward.js +65 -5
  170. package/dist/server/public/forward.js.map +1 -1
  171. package/dist/server/public/index.d.ts +1 -0
  172. package/dist/server/public/index.js +1 -0
  173. package/dist/server/public/index.js.map +1 -1
  174. package/dist/server/public/listing.d.ts +9 -2
  175. package/dist/server/public/listing.js +30 -9
  176. package/dist/server/public/listing.js.map +1 -1
  177. package/dist/server/public/responses.js +9 -5
  178. package/dist/server/public/responses.js.map +1 -1
  179. package/dist/server/public/system-messages.d.ts +39 -0
  180. package/dist/server/public/system-messages.js +166 -0
  181. package/dist/server/public/system-messages.js.map +1 -0
  182. package/dist/server/public/telemetry.d.ts +9 -0
  183. package/dist/server/public/telemetry.js +42 -6
  184. package/dist/server/public/telemetry.js.map +1 -1
  185. package/dist/server/public/thinking.d.ts +26 -0
  186. package/dist/server/public/thinking.js +62 -0
  187. package/dist/server/public/thinking.js.map +1 -0
  188. package/dist/server/public/trace.d.ts +8 -0
  189. package/dist/server/public/trace.js +17 -0
  190. package/dist/server/public/trace.js.map +1 -1
  191. package/dist/server/public/types.d.ts +8 -0
  192. package/dist/server/shims/index.d.ts +1 -0
  193. package/dist/server/shims/index.js +1 -0
  194. package/dist/server/shims/index.js.map +1 -1
  195. package/dist/server/shims/reminders.d.ts +8 -0
  196. package/dist/server/shims/reminders.js +14 -0
  197. package/dist/server/shims/reminders.js.map +1 -0
  198. package/dist/server/shims/responses.d.ts +33 -9
  199. package/dist/server/shims/responses.js +160 -36
  200. package/dist/server/shims/responses.js.map +1 -1
  201. package/dist/settings/default-migrations.d.ts +34 -0
  202. package/dist/settings/default-migrations.js +47 -0
  203. package/dist/settings/default-migrations.js.map +1 -0
  204. package/dist/settings/schema/atomic-prism.json +23 -9
  205. package/dist/settings/schema/llamacpp-upstream.json +23 -9
  206. package/dist/settings/schema/llamacpp.json +23 -9
  207. package/dist/settings/schema/sglang.json +8 -8
  208. package/dist/settings/schema/vllm.json +8 -8
  209. package/dist/settings/schema.js +1 -0
  210. package/dist/settings/schema.js.map +1 -1
  211. package/dist/settings/store.d.ts +6 -0
  212. package/dist/settings/store.js +18 -1
  213. package/dist/settings/store.js.map +1 -1
  214. package/dist/version.d.ts +1 -1
  215. package/dist/version.js +1 -1
  216. package/package.json +1 -1
@@ -3,17 +3,34 @@
3
3
  *
4
4
  * That is deliberate: the file is hand-edited, heavily commented, and its `custom_providers` list
5
5
  * carries the user's own bridges (Telegram, WhatsApp, …). A YAML round trip would drop every comment
6
- * and reflow every block for the sake of three scalars. So we rewrite only the lines we own and
6
+ * and reflow every block for the sake of a few scalars. So we rewrite only the lines we own and
7
7
  * splice our list entry in, leaving everything else byte-identical.
8
8
  *
9
- * Hermes ignores the API key entirely (`api_key` is accepted for call-site symmetry and reserved),
10
- * and `.env` is patched only when it already exists — creating one would shadow the agent's own
11
- * defaults.
9
+ * The route is bare `custom` with `model.base_url` and `model.api_key`: Hermes trusts that URL while
10
+ * `model.provider` is `custom` and takes the key from the same section, so the route needs nothing
11
+ * outside it — unlike `custom:atomic-chat`, which needs our `custom_providers` entry, and that cannot
12
+ * be added to an inline `custom_providers: [...]` list. The entry is still written where it can be,
13
+ * for `hermes model` and detection. What the edit replaced is recorded in
14
+ * `config.yaml.atomic-chat-backup.json` for the app's Disconnect. `.env` is patched only when it
15
+ * already exists — creating one would shadow the agent's own defaults.
16
+ *
17
+ * Mirrors `configure_hermes_at` in the app's `core/system/commands.rs`, byte for byte.
12
18
  */
13
19
  import type { ConfigFs } from '../config-io.js';
14
20
  import type { ConfigureWriter } from './registry.js';
15
21
  /** Rewrite everything after the first colon, with no quoting — the values here never need it. */
16
22
  export declare function replaceYamlScalarValue(line: string, value: string): string;
23
+ /**
24
+ * Point the top-level `model:` section at our endpoint, and touch nothing outside it.
25
+ *
26
+ * Hermes' chat route comes from `model.default` (`model.model` is an alias), `model.provider` and
27
+ * `model.base_url`; a section it cannot read leaves it on its built-in default, an empty model on
28
+ * provider `auto`, which falls through to OpenRouter. The same keys recur under `auxiliary:`,
29
+ * `delegation:`, `compression:` and others, so only direct children of the top-level `model:` key are
30
+ * ours. A missing key is added, a scalar `model: <id>` (also valid Hermes) becomes a block, and a file
31
+ * with no `model:` at all gets one appended.
32
+ */
33
+ export declare function patchModelSection(content: string, wanted: Array<[string, string]>): string;
17
34
  /**
18
35
  * Split the document into (before, entries, after) around `custom_providers:`. `entries` is one
19
36
  * array of raw lines per list item; blank lines inside the block are dropped, which is why a rerun
@@ -26,16 +43,52 @@ export declare function splitCustomProviders(content: string): {
26
43
  };
27
44
  /** Re-emit the block: items at column 0, or `custom_providers: []` when nothing is left. */
28
45
  export declare function rebuildCustomProviders(before: string[], entries: string[][], after: string[]): string;
29
- /** Add or update only our entry, leaving the user's other bridges untouched and in order. */
30
- export declare function upsertAtomicProvider(content: string, apiUrl: string, model: string, ctx: number): string;
46
+ /**
47
+ * Add or update only our entry, leaving the user's other bridges untouched and in order; also says
48
+ * whether the `custom_providers` key itself was added. `key` is already a YAML scalar.
49
+ */
50
+ export declare function upsertAtomicProvider(content: string, apiUrl: string, model: string, ctx: number, key: string): [string, boolean];
31
51
  /**
32
52
  * Ensure `providers.<id>.request_timeout_seconds` exists, creating the `providers:` map and the
33
53
  * provider sub-block as needed. A value the user already set is left alone — we only fill the gap.
54
+ * Also returns what it created, for Disconnect: `providers`, `providers:{}` (an empty inline map
55
+ * turned into a block), `providers.<id>` and `providers.<id>.request_timeout_seconds`.
56
+ */
57
+ export declare function upsertProviderRequestTimeout(content: string, providerId: string, seconds: number): [string, string[]];
58
+ /** One `model:` key Connect wrote: the lines it replaced (absent: none) and the value it wrote. */
59
+ interface KeyBackup {
60
+ before?: string[];
61
+ ours: string;
62
+ }
63
+ /** What Connect changed in `config.yaml` (the app's `HermesBackup`). */
64
+ export interface HermesBackup {
65
+ /** The `model:` line before Connect; absent when there was no section. */
66
+ header?: string;
67
+ keys: Record<string, KeyBackup>;
68
+ /** What Connect added that was not there at all (see `upsertProviderRequestTimeout`). */
69
+ created: string[];
70
+ }
71
+ export declare function parseHermesBackup(text: string): HermesBackup | undefined;
72
+ export declare function renderHermesBackup(backup: HermesBackup): string;
73
+ /**
74
+ * The backup of a Connect that wrote `route` over `content`. A key an earlier Connect recorded keeps
75
+ * that record's original while it still holds the value that Connect wrote; otherwise the user
76
+ * changed it since, and the current lines are the ones to return to.
77
+ */
78
+ export declare function hermesBackup(content: string, route: Array<[string, string]>, previous: HermesBackup | undefined, created: string[]): HermesBackup;
79
+ /**
80
+ * `HERMES_HOME` as Hermes reads it (`os.path.expanduser(os.path.expandvars(...))`): `$NAME`,
81
+ * `${NAME}` and `%NAME%` take the variable's value (an unset one stays as written), then a leading
82
+ * `~` is the user's home.
34
83
  */
35
- export declare function upsertProviderRequestTimeout(content: string, providerId: string, seconds: number): string;
84
+ export declare function expandHermesHome(raw: string, env: NodeJS.ProcessEnv, home: string): string;
36
85
  /**
37
- * `~/.hermes` everywhere except Windows, where the installer writes `HERMES_HOME` to the user
38
- * environment and the CLI honours it.
86
+ * The directory whose `config.yaml` the `hermes` CLI reads (`resolve_hermes_dir` in the app):
87
+ * `HERMES_HOME` (expanded) or the platform default (`%LOCALAPPDATA%/hermes` on Windows, `~/.hermes`
88
+ * elsewhere) is the root; a `HERMES_HOME` naming a profile (`<root>/profiles/<name>`) is used as it
89
+ * is; otherwise the sticky profile `hermes profile use <name>` leaves in `<root>/active_profile` is
90
+ * what every `hermes` without `-p` runs, from `<root>/profiles/<name>`.
39
91
  */
40
- export declare function hermesDir(env: NodeJS.ProcessEnv, platform: NodeJS.Platform, fs: ConfigFs): string;
92
+ export declare function hermesDir(env: NodeJS.ProcessEnv, platform: NodeJS.Platform, fs: ConfigFs): Promise<string>;
41
93
  export declare const configureHermes: ConfigureWriter;
94
+ export {};
@@ -3,29 +3,30 @@
3
3
  *
4
4
  * That is deliberate: the file is hand-edited, heavily commented, and its `custom_providers` list
5
5
  * carries the user's own bridges (Telegram, WhatsApp, …). A YAML round trip would drop every comment
6
- * and reflow every block for the sake of three scalars. So we rewrite only the lines we own and
6
+ * and reflow every block for the sake of a few scalars. So we rewrite only the lines we own and
7
7
  * splice our list entry in, leaving everything else byte-identical.
8
8
  *
9
- * Hermes ignores the API key entirely (`api_key` is accepted for call-site symmetry and reserved),
10
- * and `.env` is patched only when it already exists — creating one would shadow the agent's own
11
- * defaults.
9
+ * The route is bare `custom` with `model.base_url` and `model.api_key`: Hermes trusts that URL while
10
+ * `model.provider` is `custom` and takes the key from the same section, so the route needs nothing
11
+ * outside it — unlike `custom:atomic-chat`, which needs our `custom_providers` entry, and that cannot
12
+ * be added to an inline `custom_providers: [...]` list. The entry is still written where it can be,
13
+ * for `hermes model` and detection. What the edit replaced is recorded in
14
+ * `config.yaml.atomic-chat-backup.json` for the app's Disconnect. `.env` is patched only when it
15
+ * already exists — creating one would shadow the agent's own defaults.
16
+ *
17
+ * Mirrors `configure_hermes_at` in the app's `core/system/commands.rs`, byte for byte.
12
18
  */
13
19
  import { HERMES_CONTEXT_LENGTH } from '../catalog.js';
20
+ import { canonicalJson } from '../config-io.js';
21
+ import { BACKUP_SUFFIX, writeBackupText } from './json-key-backup.js';
14
22
  import { registerWriter } from './registry.js';
15
23
  const PROVIDER_NAME = 'atomic-chat';
24
+ /** The provider older releases wrote: our named `custom_providers` entry. */
25
+ const LEGACY_PROVIDER = `custom:${PROVIDER_NAME}`;
16
26
  /** Hermes otherwise waits out its legacy 1800s default; a tighter cap lets a wedged turn fail fast. */
17
27
  const REQUEST_TIMEOUT_SECONDS = 180;
18
- /**
19
- * Seeded when no config exists. The installer runs with `--skip-setup`, which skips the wizard that
20
- * would create one, so on a fresh install there was nothing to patch at all. This carries exactly
21
- * the anchors the patch logic below looks for.
22
- */
23
- const DEFAULT_CONFIG = `model:
24
- default: anthropic/claude-opus-4.6
25
- provider: auto
26
- base_url: https://openrouter.ai/api/v1
27
- custom_providers: []
28
- `;
28
+ const NO_PROXY_LINE = 'NO_PROXY=localhost,127.0.0.1,0.0.0.0';
29
+ const NO_PROXY_EXPORT_LINE = 'export no_proxy=localhost,127.0.0.1,0.0.0.0';
29
30
  /** Rust's `str::lines`: split on `\n`, drop the empty tail, strip a trailing `\r` from each line. */
30
31
  function lines(text) {
31
32
  const parts = text.split('\n');
@@ -41,6 +42,143 @@ export function replaceYamlScalarValue(line, value) {
41
42
  const colon = line.indexOf(':');
42
43
  return colon < 0 ? line : `${line.slice(0, colon + 1)} ${value}`;
43
44
  }
45
+ /** A column-0 mapping key: not indented, not a list item, not a comment, not blank. */
46
+ function isTopLevelYamlKey(line) {
47
+ const c = line.charAt(0);
48
+ return c !== '' && c !== ' ' && c !== '\t' && c !== '-' && c !== '#';
49
+ }
50
+ /** Drop a trailing `# comment` (YAML only starts one after whitespace). */
51
+ function stripYamlComment(text) {
52
+ return text.replace(/(^|\s)#.*$/, '');
53
+ }
54
+ /** `key: value` of a column-0 mapping line, the value without its comment; `undefined` otherwise. */
55
+ function topLevelYamlKey(line) {
56
+ if (!isTopLevelYamlKey(line))
57
+ return undefined;
58
+ const colon = line.indexOf(':');
59
+ if (colon < 0)
60
+ return undefined;
61
+ return { key: line.slice(0, colon).trim(), value: stripYamlComment(line.slice(colon + 1)).trim() };
62
+ }
63
+ function indentOf(line) {
64
+ return line.length - line.trimStart().length;
65
+ }
66
+ function isBlankOrComment(line) {
67
+ const t = line.trim();
68
+ return t === '' || t.startsWith('#');
69
+ }
70
+ function modelSection(out) {
71
+ const header = out.findIndex((line) => topLevelYamlKey(line)?.key === 'model');
72
+ if (header < 0)
73
+ return undefined;
74
+ let end = out.length;
75
+ for (let i = header + 1; i < out.length; i++) {
76
+ if (isTopLevelYamlKey(out[i])) {
77
+ end = i;
78
+ break;
79
+ }
80
+ }
81
+ return { header, end, isScalar: (topLevelYamlKey(out[header])?.value ?? '') !== '' };
82
+ }
83
+ function childIndent(out, section) {
84
+ const first = out.slice(section.header + 1, section.end).find((line) => !isBlankOrComment(line));
85
+ return first === undefined ? ' ' : first.slice(0, indentOf(first));
86
+ }
87
+ function isChildKey(line, indent, key) {
88
+ if (isBlankOrComment(line) || indentOf(line) !== indent)
89
+ return false;
90
+ const t = line.trim();
91
+ return t.startsWith(key) && t.slice(key.length).trimStart().startsWith(':');
92
+ }
93
+ /** The lines of the direct child `key` of a block section: its own line and the more-indented lines
94
+ * of a nested value after it (with any comments between those), as `[start, end)`. */
95
+ function childSpan(out, section, key) {
96
+ if (section.isScalar)
97
+ return undefined;
98
+ const indent = childIndent(out, section).length;
99
+ let start = -1;
100
+ for (let i = section.header + 1; i < section.end; i++) {
101
+ if (isChildKey(out[i], indent, key)) {
102
+ start = i;
103
+ break;
104
+ }
105
+ }
106
+ if (start < 0)
107
+ return undefined;
108
+ let end = start + 1;
109
+ for (let i = start + 1; i < section.end;) {
110
+ const line = out[i];
111
+ if (isBlankOrComment(line))
112
+ i++;
113
+ else if (indentOf(line) > indent) {
114
+ i++;
115
+ end = i;
116
+ }
117
+ else
118
+ break;
119
+ }
120
+ return [start, end];
121
+ }
122
+ /** The value text of a `key: value` line, without its comment (quotes kept). */
123
+ function scalarOf(line) {
124
+ const colon = line.indexOf(':');
125
+ return colon < 0 ? '' : stripYamlComment(line.slice(colon + 1)).trim();
126
+ }
127
+ function modelValue(out, section, key) {
128
+ const span = section === undefined ? undefined : childSpan(out, section, key);
129
+ return span === undefined ? undefined : scalarOf(out[span[0]]);
130
+ }
131
+ /**
132
+ * Point the top-level `model:` section at our endpoint, and touch nothing outside it.
133
+ *
134
+ * Hermes' chat route comes from `model.default` (`model.model` is an alias), `model.provider` and
135
+ * `model.base_url`; a section it cannot read leaves it on its built-in default, an empty model on
136
+ * provider `auto`, which falls through to OpenRouter. The same keys recur under `auxiliary:`,
137
+ * `delegation:`, `compression:` and others, so only direct children of the top-level `model:` key are
138
+ * ours. A missing key is added, a scalar `model: <id>` (also valid Hermes) becomes a block, and a file
139
+ * with no `model:` at all gets one appended.
140
+ */
141
+ export function patchModelSection(content, wanted) {
142
+ const out = lines(content);
143
+ const block = (indent) => wanted.map(([key, value]) => `${indent}${key}: ${value}`);
144
+ const found = modelSection(out);
145
+ if (found === undefined) {
146
+ while (out.length > 0 && out[out.length - 1].trim() === '')
147
+ out.pop();
148
+ out.push('model:', ...block(' '));
149
+ return endWithNewline(out.join('\n'));
150
+ }
151
+ const { header: headerIdx, end, isScalar } = found;
152
+ // Anything still indented under a scalar `model: <id>` header is that scalar's continuation.
153
+ const section = out.slice(headerIdx + 1, end).filter((line) => !isScalar || isBlankOrComment(line));
154
+ if (isScalar) {
155
+ out.splice(headerIdx, end - headerIdx, 'model:', ...block(' '), ...section);
156
+ return endWithNewline(out.join('\n'));
157
+ }
158
+ const firstChild = section.find((line) => !isBlankOrComment(line));
159
+ const indent = firstChild === undefined ? ' ' : firstChild.slice(0, indentOf(firstChild));
160
+ const missing = [];
161
+ for (const [key, value] of wanted) {
162
+ const at = section.findIndex((line) => isChildKey(line, indent.length, key));
163
+ if (at < 0) {
164
+ missing.push(`${indent}${key}: ${value}`);
165
+ continue;
166
+ }
167
+ section[at] = replaceYamlScalarValue(section[at], value);
168
+ // A nested value (a mapping-valued `default`, a wrapped scalar) would dangle under the new one.
169
+ for (let i = at + 1; i < section.length;) {
170
+ const line = section[i];
171
+ if (isBlankOrComment(line))
172
+ i++;
173
+ else if (indentOf(line) > indent.length)
174
+ section.splice(i, 1);
175
+ else
176
+ break;
177
+ }
178
+ }
179
+ out.splice(headerIdx + 1, end - headerIdx - 1, ...missing, ...section);
180
+ return endWithNewline(out.join('\n'));
181
+ }
44
182
  /**
45
183
  * Split the document into (before, entries, after) around `custom_providers:`. `entries` is one
46
184
  * array of raw lines per list item; blank lines inside the block are dropped, which is why a rerun
@@ -53,7 +191,7 @@ export function splitCustomProviders(content) {
53
191
  let phase = 'before';
54
192
  for (const line of lines(content)) {
55
193
  if (phase === 'before') {
56
- const t = line.trim();
194
+ const t = stripYamlComment(line).trim();
57
195
  if (t === 'custom_providers:' || t === 'custom_providers: []' || t === 'custom_providers:[]') {
58
196
  phase = t.includes('[]') ? 'after' : 'inBlock';
59
197
  }
@@ -114,45 +252,66 @@ export function rebuildCustomProviders(before, entries, after) {
114
252
  result.push(...after);
115
253
  return endWithNewline(result.join('\n'));
116
254
  }
117
- /** Add or update only our entry, leaving the user's other bridges untouched and in order. */
118
- export function upsertAtomicProvider(content, apiUrl, model, ctx) {
255
+ /** Whether the top-level `custom_providers:` holds an inline list with entries. */
256
+ function hasInlineCustomProviders(content) {
257
+ const inline = lines(content)
258
+ .map(topLevelYamlKey)
259
+ .find((entry) => entry?.key === 'custom_providers')?.value;
260
+ return inline !== undefined && inline !== '' && inline.replace(/\s/g, '') !== '[]';
261
+ }
262
+ /**
263
+ * Add or update only our entry, leaving the user's other bridges untouched and in order; also says
264
+ * whether the `custom_providers` key itself was added. `key` is already a YAML scalar.
265
+ */
266
+ export function upsertAtomicProvider(content, apiUrl, model, ctx, key) {
267
+ // An inline list cannot be spliced, and a second `custom_providers:` would make the whole file
268
+ // unreadable to Hermes (duplicate key); leave it rather than append one.
269
+ if (hasInlineCustomProviders(content))
270
+ return [endWithNewline(content), false];
271
+ const hadKey = lines(content).some((line) => topLevelYamlKey(line)?.key === 'custom_providers');
119
272
  const { before, entries, after } = splitCustomProviders(content);
120
273
  const kept = entries.filter((entry) => !entryIsOurs(entry));
121
274
  kept.push([
122
275
  `- name: ${PROVIDER_NAME}`,
123
276
  ` base_url: ${apiUrl}`,
277
+ ` api_key: ${key}`,
124
278
  ` model: ${model}`,
125
279
  ' models:',
126
280
  ` ${model}:`,
127
281
  ` context_length: ${ctx}`,
128
282
  ]);
129
- return rebuildCustomProviders(before, kept, after);
130
- }
131
- /** A column-0 mapping key: not indented, not a list item, not a comment, not blank. */
132
- function isTopLevelYamlKey(line) {
133
- const c = line.charAt(0);
134
- return c !== '' && c !== ' ' && c !== '\t' && c !== '-' && c !== '#';
283
+ return [rebuildCustomProviders(before, kept, after), !hadKey];
135
284
  }
136
285
  /**
137
286
  * Ensure `providers.<id>.request_timeout_seconds` exists, creating the `providers:` map and the
138
287
  * provider sub-block as needed. A value the user already set is left alone — we only fill the gap.
288
+ * Also returns what it created, for Disconnect: `providers`, `providers:{}` (an empty inline map
289
+ * turned into a block), `providers.<id>` and `providers.<id>.request_timeout_seconds`.
139
290
  */
140
291
  export function upsertProviderRequestTimeout(content, providerId, seconds) {
141
292
  const out = lines(content);
293
+ const created = [];
142
294
  const provKeyLine = ` ${providerId}:`;
143
295
  const fieldLine = ` request_timeout_seconds: ${seconds}`;
144
- const providersIdx = out.findIndex((line) => {
145
- const t = line.replace(/\s+$/, '');
146
- return isTopLevelYamlKey(line) && (t === 'providers:' || t === 'providers: {}' || t === 'providers:{}');
147
- });
148
- if (providersIdx < 0) {
296
+ const providerLabel = `providers.${providerId}`;
297
+ const fieldLabel = `providers.${providerId}.request_timeout_seconds`;
298
+ const providersIdx = out.findIndex((line) => topLevelYamlKey(line)?.key === 'providers');
299
+ const providers = providersIdx < 0 ? undefined : topLevelYamlKey(out[providersIdx]);
300
+ if (providers === undefined) {
149
301
  while (out.length > 0 && out[out.length - 1].trim() === '')
150
302
  out.pop();
151
303
  out.push('providers:', provKeyLine, fieldLine);
152
- return endWithNewline(out.join('\n'));
304
+ created.push('providers', providerLabel, fieldLabel);
305
+ return [endWithNewline(out.join('\n')), created];
153
306
  }
154
- if (out[providersIdx].replace(/\s+$/, '') !== 'providers:')
307
+ // Hermes' YAML reader rejects a duplicate key, and then runs on its defaults: an inline map we
308
+ // cannot edit line by line is left as it is rather than shadowed by a second `providers:`.
309
+ if (providers.value !== '' && providers.value.replace(/\s/g, '') !== '{}')
310
+ return [endWithNewline(out.join('\n')), created];
311
+ if (providers.value !== '') {
155
312
  out[providersIdx] = 'providers:';
313
+ created.push('providers:{}');
314
+ }
156
315
  // The providers block runs until the next column-0 key.
157
316
  let blockEnd = out.length;
158
317
  for (let i = providersIdx + 1; i < out.length; i++) {
@@ -170,7 +329,8 @@ export function upsertProviderRequestTimeout(content, providerId, seconds) {
170
329
  }
171
330
  if (provIdx < 0) {
172
331
  out.splice(providersIdx + 1, 0, provKeyLine, fieldLine);
173
- return endWithNewline(out.join('\n'));
332
+ created.push(providerLabel, fieldLabel);
333
+ return [endWithNewline(out.join('\n')), created];
174
334
  }
175
335
  // This provider's sub-block runs until the next key at indent <= 2 (a sibling provider).
176
336
  let subEnd = blockEnd;
@@ -186,62 +346,187 @@ export function upsertProviderRequestTimeout(content, providerId, seconds) {
186
346
  const hasField = out
187
347
  .slice(provIdx + 1, subEnd)
188
348
  .some((line) => line.trimStart().startsWith('request_timeout_seconds:'));
189
- if (!hasField)
349
+ if (!hasField) {
190
350
  out.splice(provIdx + 1, 0, fieldLine);
191
- return endWithNewline(out.join('\n'));
351
+ created.push(fieldLabel);
352
+ }
353
+ return [endWithNewline(out.join('\n')), created];
354
+ }
355
+ /** `value` without one pair of surrounding quotes. */
356
+ function unquote(value) {
357
+ for (const quote of ['"', "'"]) {
358
+ if (value.length >= 2 && value.startsWith(quote) && value.endsWith(quote))
359
+ return value.slice(1, -1);
360
+ }
361
+ return value;
362
+ }
363
+ /** Whether `current` is the value Connect wrote; Hermes may persist bare `custom` under our name. */
364
+ function valueIsOurs(key, current, ours) {
365
+ const value = unquote(current);
366
+ return value === unquote(ours) || (key === 'provider' && ours === 'custom' && value === LEGACY_PROVIDER);
367
+ }
368
+ export function parseHermesBackup(text) {
369
+ try {
370
+ const root = JSON.parse(text);
371
+ const created = root.created;
372
+ const keys = root.model?.keys;
373
+ if (!Array.isArray(created) || !created.every((c) => typeof c === 'string'))
374
+ return undefined;
375
+ if (typeof keys !== 'object' || keys === null)
376
+ return undefined;
377
+ const parsed = {};
378
+ for (const [key, entry] of Object.entries(keys)) {
379
+ if (typeof entry?.ours !== 'string')
380
+ return undefined;
381
+ const before = entry.before;
382
+ if (before !== undefined && !(Array.isArray(before) && before.every((l) => typeof l === 'string')))
383
+ return undefined;
384
+ parsed[key] = { ours: entry.ours, ...(before !== undefined ? { before: before } : {}) };
385
+ }
386
+ const header = root.model?.header;
387
+ return { ...(typeof header === 'string' ? { header } : {}), keys: parsed, created: created };
388
+ }
389
+ catch {
390
+ return undefined;
391
+ }
392
+ }
393
+ export function renderHermesBackup(backup) {
394
+ return canonicalJson({
395
+ created: [...new Set(backup.created)].sort(),
396
+ model: { ...(backup.header !== undefined ? { header: backup.header } : {}), keys: backup.keys },
397
+ });
398
+ }
399
+ /**
400
+ * The backup of a Connect that wrote `route` over `content`. A key an earlier Connect recorded keeps
401
+ * that record's original while it still holds the value that Connect wrote; otherwise the user
402
+ * changed it since, and the current lines are the ones to return to.
403
+ */
404
+ export function hermesBackup(content, route, previous, created) {
405
+ const out = lines(content);
406
+ const section = modelSection(out);
407
+ const stillOurs = (prev) => Object.entries(prev.keys).every(([key, entry]) => {
408
+ const value = modelValue(out, section, key);
409
+ return value !== undefined && valueIsOurs(key, value, entry.ours);
410
+ });
411
+ const header = previous !== undefined && stillOurs(previous)
412
+ ? previous.header
413
+ : section === undefined
414
+ ? undefined
415
+ : out[section.header];
416
+ const keys = {};
417
+ for (const [key, ours] of route) {
418
+ const earlier = previous?.keys[key];
419
+ const current = modelValue(out, section, key);
420
+ let before;
421
+ if (earlier !== undefined && current !== undefined && valueIsOurs(key, current, earlier.ours)) {
422
+ before = earlier.before;
423
+ }
424
+ else {
425
+ const span = section === undefined ? undefined : childSpan(out, section, key);
426
+ before =
427
+ span === undefined ? undefined : out.slice(span[0], span[1]).filter((l) => !isBlankOrComment(l));
428
+ }
429
+ keys[key] = { ours, ...(before !== undefined ? { before } : {}) };
430
+ }
431
+ return {
432
+ ...(header !== undefined ? { header } : {}),
433
+ keys,
434
+ created: [...new Set([...(previous?.created ?? []), ...created])].sort(),
435
+ };
436
+ }
437
+ // ── where Hermes reads its config ───────────────────────────────────────────────────────────────
438
+ /**
439
+ * `HERMES_HOME` as Hermes reads it (`os.path.expanduser(os.path.expandvars(...))`): `$NAME`,
440
+ * `${NAME}` and `%NAME%` take the variable's value (an unset one stays as written), then a leading
441
+ * `~` is the user's home.
442
+ */
443
+ export function expandHermesHome(raw, env, home) {
444
+ const value = (name) => {
445
+ const v = env[name];
446
+ return v !== undefined && v.trim() !== '' ? v : undefined;
447
+ };
448
+ const expanded = raw.replace(/\$\{([^}]*)\}|\$([A-Za-z0-9_]+)|%([^%]+)%/g, (match, braced, bare, percent) => value((braced ?? bare ?? percent)) ?? match);
449
+ if (expanded === '~')
450
+ return home;
451
+ if (expanded.startsWith('~/') || expanded.startsWith('~\\'))
452
+ return `${home}${expanded.slice(1)}`;
453
+ return expanded;
192
454
  }
455
+ const parentName = (path) => path
456
+ .split(/[\\/]+/)
457
+ .filter(Boolean)
458
+ .slice(-2, -1)[0];
193
459
  /**
194
- * `~/.hermes` everywhere except Windows, where the installer writes `HERMES_HOME` to the user
195
- * environment and the CLI honours it.
460
+ * The directory whose `config.yaml` the `hermes` CLI reads (`resolve_hermes_dir` in the app):
461
+ * `HERMES_HOME` (expanded) or the platform default (`%LOCALAPPDATA%/hermes` on Windows, `~/.hermes`
462
+ * elsewhere) is the root; a `HERMES_HOME` naming a profile (`<root>/profiles/<name>`) is used as it
463
+ * is; otherwise the sticky profile `hermes profile use <name>` leaves in `<root>/active_profile` is
464
+ * what every `hermes` without `-p` runs, from `<root>/profiles/<name>`.
196
465
  */
197
- export function hermesDir(env, platform, fs) {
198
- if (platform !== 'win32')
199
- return '.hermes';
200
- const explicit = env['HERMES_HOME'];
201
- if (explicit && explicit !== '')
202
- return explicit;
466
+ export async function hermesDir(env, platform, fs) {
203
467
  const localAppData = env['LOCALAPPDATA'];
204
- return localAppData && localAppData !== '' ? `${localAppData}/hermes` : fs.absolute('hermes');
468
+ const native = platform === 'win32'
469
+ ? `${localAppData && localAppData.trim() !== '' ? localAppData : `${fs.home}/AppData/Local`}/hermes`
470
+ : '.hermes';
471
+ const raw = env['HERMES_HOME']?.trim();
472
+ const envHome = raw ? expandHermesHome(raw, env, fs.home) : undefined;
473
+ if (envHome !== undefined && parentName(envHome) === 'profiles')
474
+ return envHome;
475
+ // The root holding `active_profile`: the platform default for anything under it, else
476
+ // `HERMES_HOME` itself (Docker and custom roots).
477
+ const nativeAbs = fs.absolute(native).replace(/[\\/]+$/, '');
478
+ const under = envHome !== undefined &&
479
+ (fs.absolute(envHome) === nativeAbs ||
480
+ fs.absolute(envHome).startsWith(`${nativeAbs}/`) ||
481
+ fs.absolute(envHome).startsWith(`${nativeAbs}\\`));
482
+ const root = envHome !== undefined && !under ? envHome : native;
483
+ const base = envHome ?? native;
484
+ const active = ((await fs.read(`${root}/active_profile`)) ?? '')
485
+ .replace(/^\uFEFF/, '')
486
+ .trim()
487
+ .toLowerCase();
488
+ if (active !== '' && active !== 'default') {
489
+ const profile = `${base}/profiles/${active}`;
490
+ if (await fs.exists(profile))
491
+ return profile;
492
+ }
493
+ return base;
205
494
  }
206
- export const configureHermes = async ({ apiUrl, model, fs, env, platform }) => {
207
- const dir = hermesDir(env, platform, fs);
495
+ export const configureHermes = async ({ apiUrl, model, apiKey, fs, env, platform, contextLength, }) => {
496
+ const dir = await hermesDir(env, platform, fs);
208
497
  const configPath = `${dir}/config.yaml`;
209
498
  const envPath = `${dir}/.env`;
210
- let content = await fs.read(configPath);
211
- if (content === undefined) {
212
- await fs.write(configPath, DEFAULT_CONFIG);
213
- content = DEFAULT_CONFIG;
214
- }
215
- // Only the FIRST occurrence of each key is ours: a later `default:` belongs to some other block.
216
- let didDefault = false;
217
- let didProvider = false;
218
- let didBaseUrl = false;
219
- const patched = lines(content).map((line) => {
220
- const t = line.trim();
221
- if (!didDefault && t.startsWith('default:')) {
222
- didDefault = true;
223
- return replaceYamlScalarValue(line, model);
224
- }
225
- if (!didProvider && t.startsWith('provider:')) {
226
- didProvider = true;
227
- return replaceYamlScalarValue(line, 'custom');
228
- }
229
- if (!didBaseUrl && t.startsWith('base_url:')) {
230
- didBaseUrl = true;
231
- return replaceYamlScalarValue(line, apiUrl);
232
- }
233
- return line;
234
- });
235
- // Hermes refuses any model whose context window is below 64K, so the floor is not negotiable.
236
- const withProvider = upsertAtomicProvider(patched.join('\n'), apiUrl, model, HERMES_CONTEXT_LENGTH);
237
- const withTimeout = upsertProviderRequestTimeout(withProvider, 'custom', REQUEST_TIMEOUT_SECONDS);
238
- await fs.write(configPath, content.endsWith('\n') ? endWithNewline(withTimeout) : withTimeout);
499
+ // The installer runs with `--skip-setup`, so a fresh install may have no config at all; Hermes
500
+ // fills every key we do not write with its own defaults.
501
+ const content = (await fs.read(configPath)) ?? '';
502
+ const previousText = await fs.read(`${configPath}${BACKUP_SUFFIX}`);
503
+ const previous = previousText === undefined ? undefined : parseHermesBackup(previousText);
504
+ // Hermes refuses any model whose window is below 64K, so an unknown window is written as that floor;
505
+ // a known one is written as served.
506
+ const ctx = contextLength ?? HERMES_CONTEXT_LENGTH;
507
+ const key = JSON.stringify(apiKey === '' ? 'atomic' : apiKey);
508
+ const route = [
509
+ ['default', model],
510
+ ['provider', 'custom'],
511
+ ['base_url', apiUrl],
512
+ ['api_key', key],
513
+ ['context_length', String(ctx)],
514
+ ];
515
+ const patched = patchModelSection(content, route);
516
+ const [withProvider, addedList] = upsertAtomicProvider(patched, apiUrl, model, ctx, key);
517
+ const [withTimeout, created] = upsertProviderRequestTimeout(withProvider, 'custom', REQUEST_TIMEOUT_SECONDS);
518
+ if (addedList)
519
+ created.push('custom_providers');
239
520
  // Bypass a system proxy for loopback. Only ever a patch: we never create the file.
240
521
  const envContent = await fs.read(envPath);
241
522
  if (envContent !== undefined && !envContent.includes('NO_PROXY=') && !envContent.includes('no_proxy=')) {
242
523
  const separator = envContent.endsWith('\n') ? '' : '\n';
243
- await fs.write(envPath, `${envContent}${separator}\nNO_PROXY=localhost,127.0.0.1,0.0.0.0\nexport no_proxy=localhost,127.0.0.1,0.0.0.0`);
524
+ await fs.write(envPath, `${envContent}${separator}\n${NO_PROXY_LINE}\n${NO_PROXY_EXPORT_LINE}`);
525
+ created.push('env.no_proxy');
244
526
  }
527
+ const backup = hermesBackup(content, route, previous, created);
528
+ await fs.write(configPath, withTimeout);
529
+ await writeBackupText(fs, configPath, renderHermesBackup(backup));
245
530
  };
246
531
  registerWriter('hermes', configureHermes);
247
532
  //# sourceMappingURL=hermes.js.map