@joingonka/setup 0.1.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 (51) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +46 -0
  3. package/dist/adapters/claude-code.d.ts +3 -0
  4. package/dist/adapters/claude-code.d.ts.map +1 -0
  5. package/dist/adapters/claude-code.js +70 -0
  6. package/dist/adapters/claude-code.js.map +1 -0
  7. package/dist/adapters/cline.d.ts +3 -0
  8. package/dist/adapters/cline.d.ts.map +1 -0
  9. package/dist/adapters/cline.js +43 -0
  10. package/dist/adapters/cline.js.map +1 -0
  11. package/dist/adapters/openclaw.d.ts +3 -0
  12. package/dist/adapters/openclaw.d.ts.map +1 -0
  13. package/dist/adapters/openclaw.js +159 -0
  14. package/dist/adapters/openclaw.js.map +1 -0
  15. package/dist/adapters/registry.d.ts +24 -0
  16. package/dist/adapters/registry.d.ts.map +1 -0
  17. package/dist/adapters/registry.js +25 -0
  18. package/dist/adapters/registry.js.map +1 -0
  19. package/dist/adapters/types.d.ts +51 -0
  20. package/dist/adapters/types.d.ts.map +1 -0
  21. package/dist/adapters/types.js +10 -0
  22. package/dist/adapters/types.js.map +1 -0
  23. package/dist/cli.d.ts +3 -0
  24. package/dist/cli.d.ts.map +1 -0
  25. package/dist/cli.js +55 -0
  26. package/dist/cli.js.map +1 -0
  27. package/dist/constants.d.ts +98 -0
  28. package/dist/constants.d.ts.map +1 -0
  29. package/dist/constants.js +117 -0
  30. package/dist/constants.js.map +1 -0
  31. package/dist/core/fs-ops.d.ts +28 -0
  32. package/dist/core/fs-ops.d.ts.map +1 -0
  33. package/dist/core/fs-ops.js +63 -0
  34. package/dist/core/fs-ops.js.map +1 -0
  35. package/dist/core/merge.d.ts +62 -0
  36. package/dist/core/merge.d.ts.map +1 -0
  37. package/dist/core/merge.js +98 -0
  38. package/dist/core/merge.js.map +1 -0
  39. package/dist/core/prompt.d.ts +8 -0
  40. package/dist/core/prompt.d.ts.map +1 -0
  41. package/dist/core/prompt.js +29 -0
  42. package/dist/core/prompt.js.map +1 -0
  43. package/dist/core/validate.d.ts +12 -0
  44. package/dist/core/validate.d.ts.map +1 -0
  45. package/dist/core/validate.js +22 -0
  46. package/dist/core/validate.js.map +1 -0
  47. package/dist/run.d.ts +24 -0
  48. package/dist/run.d.ts.map +1 -0
  49. package/dist/run.js +65 -0
  50. package/dist/run.js.map +1 -0
  51. package/package.json +53 -0
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Контракт адаптера инструмента.
3
+ *
4
+ * Каждый поддерживаемый инструмент (Claude Code, OpenClaw, Cline) реализует
5
+ * Adapter. Общий core (fs-ops, merge, validate) не знает о конкретных
6
+ * инструментах — он работает через этот интерфейс. Добавление нового
7
+ * инструмента = новый адаптер + запись в registry, без правок оркестратора.
8
+ */
9
+ /**
10
+ * Куда устанавливаем конфигурацию:
11
+ * - 'user' — глобально (домашний каталог пользователя)
12
+ * - 'local' — в текущем проекте (cwd)
13
+ */
14
+ export type Scope = 'user' | 'local';
15
+ /** Вход для apply(): что именно прописать в конфиг инструмента. */
16
+ export interface ApplyInput {
17
+ apiKey: string;
18
+ model: string;
19
+ scope: Scope;
20
+ }
21
+ /** Результат apply(): что произошло, для вывода пользователю. */
22
+ export interface ApplyResult {
23
+ /** Путь к записанному конфигу, либо null для instructions-only инструментов. */
24
+ configPath: string | null;
25
+ /** Путь к созданному бэкапу, либо null если бэкапить было нечего. */
26
+ backupPath: string | null;
27
+ /** Был ли реально записан файл (false для instructions-only). */
28
+ wrote: boolean;
29
+ /** Сообщения пользователю (инструкции, подтверждения, заметки). */
30
+ messages: string[];
31
+ }
32
+ /** Адаптер одного инструмента. */
33
+ export interface Adapter {
34
+ /** Машинный идентификатор: 'claude-code' | 'openclaw' | 'cline'. */
35
+ readonly id: string;
36
+ /** Человекочитаемое имя для меню выбора. */
37
+ readonly label: string;
38
+ /** Формат конфига — определяет ветку записи. */
39
+ readonly format: 'json' | 'yaml' | 'instructions';
40
+ /**
41
+ * Абсолютный путь к целевому файлу конфига для заданного scope,
42
+ * либо null для instructions-only инструментов (ничего не пишем).
43
+ *
44
+ * ВАЖНО: homedir()/cwd() читаются В МОМЕНТ ВЫЗОВА, а не на импорте
45
+ * модуля — иначе тесты не смогут подменять HOME/cwd.
46
+ */
47
+ resolvePath(scope: Scope): string | null;
48
+ /** Применить конфигурацию (записать файл либо вернуть инструкции). */
49
+ apply(input: ApplyInput): Promise<ApplyResult>;
50
+ }
51
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAErC,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,KAAK,CAAC;CACd;AAED,iEAAiE;AACjE,MAAM,WAAW,WAAW;IAC1B,gFAAgF;IAChF,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,qEAAqE;IACrE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,iEAAiE;IACjE,KAAK,EAAE,OAAO,CAAC;IACf,mEAAmE;IACnE,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,kCAAkC;AAClC,MAAM,WAAW,OAAO;IACtB,oEAAoE;IACpE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,gDAAgD;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;IAElD;;;;;;OAMG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,MAAM,GAAG,IAAI,CAAC;IAEzC,sEAAsE;IACtE,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAChD"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Контракт адаптера инструмента.
3
+ *
4
+ * Каждый поддерживаемый инструмент (Claude Code, OpenClaw, Cline) реализует
5
+ * Adapter. Общий core (fs-ops, merge, validate) не знает о конкретных
6
+ * инструментах — он работает через этот интерфейс. Добавление нового
7
+ * инструмента = новый адаптер + запись в registry, без правок оркестратора.
8
+ */
9
+ export {};
10
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/adapters/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI entry point для @joingonka/setup.
4
+ *
5
+ * Тонкая обёртка над run() из run.ts: парсинг аргументов, подключение
6
+ * реальных промптов (core/prompt.ts) и обработка ошибок. Вся логика
7
+ * установки — внутри run().
8
+ *
9
+ * Безопасность: ключ НИКОГДА не принимается CLI-аргументом. В интерактиве
10
+ * он вводится password-промптом, в non-interactive — берётся из env
11
+ * JOINGONKA_API_KEY. Аргумент командной строки попал бы в историю shell
12
+ * и в список процессов — это утечка секрета.
13
+ */
14
+ import { Command } from 'commander';
15
+ import { run } from './run.js';
16
+ import { askApiKey, askTool } from './core/prompt.js';
17
+ const program = new Command();
18
+ program
19
+ .name('joingonka-setup')
20
+ .description('Point an agentic AI tool (Claude Code, OpenClaw, Cline) at JoinGonka Gateway')
21
+ .version('0.1.0')
22
+ .option('--tool <tool>', 'Tool to configure: claude-code | openclaw | cline')
23
+ .option('--scope <scope>', 'Installation scope: user or local', 'user')
24
+ .option('--model <model>', 'Model id, or "kimi" for Kimi K2.6 (default: Qwen3-235B)')
25
+ .option('--non-interactive', 'Do not prompt; read the API key from JOINGONKA_API_KEY env var')
26
+ .action(async (opts) => {
27
+ try {
28
+ const { result } = await run({
29
+ tool: opts.tool,
30
+ scope: opts.scope,
31
+ model: opts.model,
32
+ nonInteractive: opts.nonInteractive,
33
+ }, { askTool, askApiKey });
34
+ // Печатаем сообщения адаптера (пути/инструкции/подтверждения)
35
+ console.log('');
36
+ for (const line of result.messages) {
37
+ console.log(line);
38
+ }
39
+ if (result.backupPath) {
40
+ console.log(`Backup saved: ${result.backupPath}`);
41
+ }
42
+ process.exit(0);
43
+ }
44
+ catch (e) {
45
+ // ExitPromptError выбрасывается @inquirer/prompts при Ctrl+C
46
+ if (e instanceof Error && e.name === 'ExitPromptError') {
47
+ console.error('\nAborted.');
48
+ process.exit(130);
49
+ }
50
+ console.error('Setup failed:', e.message);
51
+ process.exit(1);
52
+ }
53
+ });
54
+ program.parse();
55
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAUtD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAC9B,OAAO;KACJ,IAAI,CAAC,iBAAiB,CAAC;KACvB,WAAW,CACV,8EAA8E,CAC/E;KACA,OAAO,CAAC,OAAO,CAAC;KAChB,MAAM,CAAC,eAAe,EAAE,mDAAmD,CAAC;KAC5E,MAAM,CAAC,iBAAiB,EAAE,mCAAmC,EAAE,MAAM,CAAC;KACtE,MAAM,CAAC,iBAAiB,EAAE,yDAAyD,CAAC;KACpF,MAAM,CACL,mBAAmB,EACnB,gEAAgE,CACjE;KACA,MAAM,CAAC,KAAK,EAAE,IAAgB,EAAE,EAAE;IACjC,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,CAC1B;YACE,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,EACD,EAAE,OAAO,EAAE,SAAS,EAAE,CACvB,CAAC;QAEF,8DAA8D;QAC9D,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAChB,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,QAAQ,EAAE,CAAC;YACnC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,MAAM,CAAC,UAAU,EAAE,CAAC;YACtB,OAAO,CAAC,GAAG,CAAC,iBAAiB,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,6DAA6D;QAC7D,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,iBAAiB,EAAE,CAAC;YACvD,OAAO,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;YAC5B,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,OAAO,CAAC,KAAK,CAAC,eAAe,EAAG,CAAW,CAAC,OAAO,CAAC,CAAC;QACrD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC,CAAC,CAAC;AACL,OAAO,CAAC,KAAK,EAAE,CAAC"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Общие константы установщика @joingonka/setup.
3
+ *
4
+ * Базовые URL и модели захардкожены и НЕ конфигурируются через CLI:
5
+ * всё ценностное предложение пакета — одна команда, перенаправляющая
6
+ * агентный инструмент на наш managed-gateway. Флаг перезаписи URL
7
+ * обесценил бы пакет и увеличил риск ошибок.
8
+ */
9
+ /**
10
+ * Base URL для Anthropic-совместимых инструментов (Claude Code и т.п.).
11
+ * БЕЗ суффикса /v1 — Claude Code сам добавляет /v1/messages.
12
+ */
13
+ export declare const BASE_URL = "https://gate.joingonka.ai";
14
+ /**
15
+ * Base URL для OpenAI-совместимых инструментов (OpenClaw, Cline и т.п.).
16
+ * С суффиксом /v1 — клиенты дописывают /chat/completions к нему.
17
+ *
18
+ * ВАЖНО: отличие /v1 vs без /v1 между двумя семействами инструментов —
19
+ * частый источник ошибок конфигурации, поэтому константы разделены явно.
20
+ */
21
+ export declare const BASE_URL_OPENAI = "https://gate.joingonka.ai/v1";
22
+ /**
23
+ * Модель по умолчанию — Qwen3-235B. Сильная для всего агентного пайплайна,
24
+ * нативный tool calling. Совпадает с примерами в knowledge-статьях.
25
+ */
26
+ export declare const DEFAULT_MODEL = "Qwen/Qwen3-235B-A22B-Instruct-2507-FP8";
27
+ /**
28
+ * Альтернативная модель Kimi K2.6 — длинный контекст.
29
+ * Выбирается через CLI `--model kimi`.
30
+ */
31
+ export declare const KIMI_MODEL = "moonshotai/Kimi-K2.6";
32
+ /**
33
+ * Идентификатор нашего провайдера внутри `models.providers` OpenClaw-конфига.
34
+ * Под этим ключом адаптер upsert-ит блок провайдера, не трогая чужие
35
+ * (`openai`, `anthropic` и т.п.).
36
+ */
37
+ export declare const OPENCLAW_PROVIDER_ID = "gonka";
38
+ /**
39
+ * Имя env-переменной, ИЗ которой OpenClaw читает наш ключ во время работы.
40
+ * В JSON-конфиг пишется именно ЭТА СТРОКА (`"apiKey": "GONKA_API_KEY"`), а НЕ
41
+ * сам секрет jg-... — ключ не должен попадать в файл на диске. Пользователю
42
+ * выдаётся инструкция `export GONKA_API_KEY=jg-...`.
43
+ */
44
+ export declare const OPENCLAW_API_KEY_ENV = "GONKA_API_KEY";
45
+ /**
46
+ * Транспорт провайдера для OpenClaw. Подключаемся в OpenAI-режиме через наш
47
+ * зрелый роут /v1/chat/completions (как GonkaGate), поэтому
48
+ * `openai-completions`. baseUrl при этом — С суффиксом /v1 (BASE_URL_OPENAI).
49
+ *
50
+ * Поле `auth` НЕ пишем: GonkaGate в OpenAI-режиме его не указывает (клиент
51
+ * по умолчанию шлёт Bearer-ключ из apiKey-переменной).
52
+ */
53
+ export declare const OPENCLAW_PROVIDER_API = "openai-completions";
54
+ /**
55
+ * Запись одной модели для каталога провайдера OpenClaw.
56
+ *
57
+ * id — каноничный id модели (casing как в SSOT model-specs.ts,
58
+ * например `moonshotai/Kimi-K2.6` — НЕ lowercase).
59
+ * name — человекочитаемое имя в UI выбора модели OpenClaw.
60
+ * maxTokens — потолок выдачи (совпадает с max_output из SSOT).
61
+ * online — true для `:online`-варианта (веб-поиск через наш плагин).
62
+ * aliasFor — для базовых моделей: короткий alias в agents.defaults.models.
63
+ * undefined у `:online`-вариантов (для них alias не заводим).
64
+ */
65
+ export interface OpenClawModelSpec {
66
+ id: string;
67
+ name: string;
68
+ maxTokens: number;
69
+ online: boolean;
70
+ /** Короткий alias (только для базовых вариантов); undefined для :online. */
71
+ aliasFor?: string;
72
+ }
73
+ /**
74
+ * Каталог моделей, которые адаптер прописывает в OpenClaw-провайдер `gonka`.
75
+ *
76
+ * Три базовых модели (с alias) + три `:online`-варианта (веб-поиск). Порядок
77
+ * совпадает с рабочим конфигом оператора: Kimi — первой (она же primary).
78
+ * SSOT по id/maxTokens — gateway/src/modules/network-status/model-specs.ts.
79
+ */
80
+ export declare const OPENCLAW_MODELS: readonly OpenClawModelSpec[];
81
+ /**
82
+ * Модель по умолчанию (primary) для OpenClaw в формате провайдер-ref
83
+ * `<provider>/<modelId>`. Ставится в agents.defaults.model.primary ТОЛЬКО
84
+ * если пользователь ещё не задал свой primary.
85
+ */
86
+ export declare const OPENCLAW_DEFAULT_PRIMARY = "gonka/moonshotai/Kimi-K2.6";
87
+ /**
88
+ * Сборка JSON-записи модели для каталога провайдера OpenClaw.
89
+ * Вынесено сюда, чтобы форма записи (cost/input/contextWindow) была SSOT
90
+ * и не дублировалась в адаптере/тестах.
91
+ */
92
+ export declare function openclawModelEntry(spec: OpenClawModelSpec): Record<string, unknown>;
93
+ /**
94
+ * Provider-ref модели (`<provider>/<modelId>`) для agents.defaults.models
95
+ * и primary. Применяется к базовому id (без суффикса `:online`).
96
+ */
97
+ export declare function openclawModelRef(modelId: string): string;
98
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,eAAO,MAAM,QAAQ,8BAA8B,CAAC;AAEpD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,iCAAiC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,aAAa,2CAA2C,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,UAAU,yBAAyB,CAAC;AAMjD;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,UAAU,CAAC;AAE5C;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,kBAAkB,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAE1D;;;;;;;;;;GAUG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAqBD;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,iBAAiB,EAOvD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,+BAAiD,CAAC;AAEvF;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASnF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD"}
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Общие константы установщика @joingonka/setup.
3
+ *
4
+ * Базовые URL и модели захардкожены и НЕ конфигурируются через CLI:
5
+ * всё ценностное предложение пакета — одна команда, перенаправляющая
6
+ * агентный инструмент на наш managed-gateway. Флаг перезаписи URL
7
+ * обесценил бы пакет и увеличил риск ошибок.
8
+ */
9
+ /**
10
+ * Base URL для Anthropic-совместимых инструментов (Claude Code и т.п.).
11
+ * БЕЗ суффикса /v1 — Claude Code сам добавляет /v1/messages.
12
+ */
13
+ export const BASE_URL = 'https://gate.joingonka.ai';
14
+ /**
15
+ * Base URL для OpenAI-совместимых инструментов (OpenClaw, Cline и т.п.).
16
+ * С суффиксом /v1 — клиенты дописывают /chat/completions к нему.
17
+ *
18
+ * ВАЖНО: отличие /v1 vs без /v1 между двумя семействами инструментов —
19
+ * частый источник ошибок конфигурации, поэтому константы разделены явно.
20
+ */
21
+ export const BASE_URL_OPENAI = 'https://gate.joingonka.ai/v1';
22
+ /**
23
+ * Модель по умолчанию — Qwen3-235B. Сильная для всего агентного пайплайна,
24
+ * нативный tool calling. Совпадает с примерами в knowledge-статьях.
25
+ */
26
+ export const DEFAULT_MODEL = 'Qwen/Qwen3-235B-A22B-Instruct-2507-FP8';
27
+ /**
28
+ * Альтернативная модель Kimi K2.6 — длинный контекст.
29
+ * Выбирается через CLI `--model kimi`.
30
+ */
31
+ export const KIMI_MODEL = 'moonshotai/Kimi-K2.6';
32
+ // ────────────────────────────────────────────────────────────────────────────
33
+ // OpenClaw-специфичные константы (JSON-конфиг ~/.openclaw/openclaw.json)
34
+ // ────────────────────────────────────────────────────────────────────────────
35
+ /**
36
+ * Идентификатор нашего провайдера внутри `models.providers` OpenClaw-конфига.
37
+ * Под этим ключом адаптер upsert-ит блок провайдера, не трогая чужие
38
+ * (`openai`, `anthropic` и т.п.).
39
+ */
40
+ export const OPENCLAW_PROVIDER_ID = 'gonka';
41
+ /**
42
+ * Имя env-переменной, ИЗ которой OpenClaw читает наш ключ во время работы.
43
+ * В JSON-конфиг пишется именно ЭТА СТРОКА (`"apiKey": "GONKA_API_KEY"`), а НЕ
44
+ * сам секрет jg-... — ключ не должен попадать в файл на диске. Пользователю
45
+ * выдаётся инструкция `export GONKA_API_KEY=jg-...`.
46
+ */
47
+ export const OPENCLAW_API_KEY_ENV = 'GONKA_API_KEY';
48
+ /**
49
+ * Транспорт провайдера для OpenClaw. Подключаемся в OpenAI-режиме через наш
50
+ * зрелый роут /v1/chat/completions (как GonkaGate), поэтому
51
+ * `openai-completions`. baseUrl при этом — С суффиксом /v1 (BASE_URL_OPENAI).
52
+ *
53
+ * Поле `auth` НЕ пишем: GonkaGate в OpenAI-режиме его не указывает (клиент
54
+ * по умолчанию шлёт Bearer-ключ из apiKey-переменной).
55
+ */
56
+ export const OPENCLAW_PROVIDER_API = 'openai-completions';
57
+ /**
58
+ * Общий context window всех текущих MoE-моделей сети (совпадает с SSOT).
59
+ */
60
+ const OPENCLAW_CONTEXT_WINDOW = 131072;
61
+ /**
62
+ * Единая тарификация для каталога OpenClaw ($/1M токенов).
63
+ *
64
+ * Значения справочные — реальная тарификация считается на нашей стороне
65
+ * (gateway), здесь они нужны лишь чтобы OpenClaw показывал примерную
66
+ * стоимость в UI. cacheRead/cacheWrite приравнены к input.
67
+ */
68
+ const OPENCLAW_COST = {
69
+ input: 0.07,
70
+ output: 0.1,
71
+ cacheRead: 0.07,
72
+ cacheWrite: 0.07,
73
+ };
74
+ /**
75
+ * Каталог моделей, которые адаптер прописывает в OpenClaw-провайдер `gonka`.
76
+ *
77
+ * Три базовых модели (с alias) + три `:online`-варианта (веб-поиск). Порядок
78
+ * совпадает с рабочим конфигом оператора: Kimi — первой (она же primary).
79
+ * SSOT по id/maxTokens — gateway/src/modules/network-status/model-specs.ts.
80
+ */
81
+ export const OPENCLAW_MODELS = [
82
+ { id: 'moonshotai/Kimi-K2.6', name: 'Kimi K2.6 (Gonka)', maxTokens: 3072, online: false, aliasFor: 'kimi-k2.6' },
83
+ { id: 'moonshotai/Kimi-K2.6:online', name: 'Kimi K2.6 + web (Gonka)', maxTokens: 3072, online: true },
84
+ { id: 'Qwen/Qwen3-235B-A22B-Instruct-2507-FP8', name: 'Qwen3-235B-A22B (Gonka)', maxTokens: 8192, online: false, aliasFor: 'qwen3-235b' },
85
+ { id: 'Qwen/Qwen3-235B-A22B-Instruct-2507-FP8:online', name: 'Qwen3-235B-A22B + web (Gonka)', maxTokens: 8192, online: true },
86
+ { id: 'MiniMaxAI/MiniMax-M2.7', name: 'MiniMax M2.7 (Gonka)', maxTokens: 4096, online: false, aliasFor: 'minimax-m2.7' },
87
+ { id: 'MiniMaxAI/MiniMax-M2.7:online', name: 'MiniMax M2.7 + web (Gonka)', maxTokens: 4096, online: true },
88
+ ];
89
+ /**
90
+ * Модель по умолчанию (primary) для OpenClaw в формате провайдер-ref
91
+ * `<provider>/<modelId>`. Ставится в agents.defaults.model.primary ТОЛЬКО
92
+ * если пользователь ещё не задал свой primary.
93
+ */
94
+ export const OPENCLAW_DEFAULT_PRIMARY = `${OPENCLAW_PROVIDER_ID}/moonshotai/Kimi-K2.6`;
95
+ /**
96
+ * Сборка JSON-записи модели для каталога провайдера OpenClaw.
97
+ * Вынесено сюда, чтобы форма записи (cost/input/contextWindow) была SSOT
98
+ * и не дублировалась в адаптере/тестах.
99
+ */
100
+ export function openclawModelEntry(spec) {
101
+ return {
102
+ id: spec.id,
103
+ name: spec.name,
104
+ input: ['text'],
105
+ contextWindow: OPENCLAW_CONTEXT_WINDOW,
106
+ maxTokens: spec.maxTokens,
107
+ cost: { ...OPENCLAW_COST },
108
+ };
109
+ }
110
+ /**
111
+ * Provider-ref модели (`<provider>/<modelId>`) для agents.defaults.models
112
+ * и primary. Применяется к базовому id (без суффикса `:online`).
113
+ */
114
+ export function openclawModelRef(modelId) {
115
+ return `${OPENCLAW_PROVIDER_ID}/${modelId}`;
116
+ }
117
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,2BAA2B,CAAC;AAEpD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,8BAA8B,CAAC;AAE9D;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,wCAAwC,CAAC;AAEtE;;;GAGG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,sBAAsB,CAAC;AAEjD,+EAA+E;AAC/E,yEAAyE;AACzE,+EAA+E;AAE/E;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,OAAO,CAAC;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,eAAe,CAAC;AAEpD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,oBAAoB,CAAC;AAsB1D;;GAEG;AACH,MAAM,uBAAuB,GAAG,MAAM,CAAC;AAEvC;;;;;;GAMG;AACH,MAAM,aAAa,GAAG;IACpB,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,GAAG;IACX,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,IAAI;CACR,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAiC;IAC3D,EAAE,EAAE,EAAE,sBAAsB,EAAE,IAAI,EAAE,mBAAmB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE;IAChH,EAAE,EAAE,EAAE,6BAA6B,EAAE,IAAI,EAAE,yBAAyB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACrG,EAAE,EAAE,EAAE,wCAAwC,EAAE,IAAI,EAAE,yBAAyB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE;IACzI,EAAE,EAAE,EAAE,+CAA+C,EAAE,IAAI,EAAE,+BAA+B,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IAC7H,EAAE,EAAE,EAAE,wBAAwB,EAAE,IAAI,EAAE,sBAAsB,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE;IACxH,EAAE,EAAE,EAAE,+BAA+B,EAAE,IAAI,EAAE,4BAA4B,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;CAC3G,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,GAAG,oBAAoB,uBAAuB,CAAC;AAEvF;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAuB;IACxD,OAAO;QACL,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,KAAK,EAAE,CAAC,MAAM,CAAC;QACf,aAAa,EAAE,uBAAuB;QACtC,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,IAAI,EAAE,EAAE,GAAG,aAAa,EAAE;KAC3B,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,GAAG,oBAAoB,IAAI,OAAO,EAAE,CAAC;AAC9C,CAAC"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Читает файл как строку. Возвращает null, если файла нет.
3
+ * Бросает только на реальных ошибках ввода-вывода (права и т.п.).
4
+ */
5
+ export declare function readRaw(filePath: string): string | null;
6
+ /**
7
+ * Создаёт бэкап файла с таймстемпом, безопасным для файловых систем.
8
+ *
9
+ * Имя: `<path>.bak.<ISO-с-замещёнными-:.>`. Двоеточия и точки в ISO-метке
10
+ * заменяются на `-`, иначе имя ломается на Windows/некоторых FS.
11
+ *
12
+ * Возвращает путь к бэкапу, либо null если исходного файла нет
13
+ * (бэкапить нечего — это не ошибка).
14
+ */
15
+ export declare function backup(filePath: string): string | null;
16
+ /**
17
+ * Атомарная запись: гарантирует существование родительского каталога,
18
+ * затем пишет через write-file-atomic (tmp + rename), чтобы файл никогда
19
+ * не оставался в полу-записанном состоянии.
20
+ *
21
+ * Опциональный `mode` задаёт права создаваемого файла. Мы НЕ полагаемся
22
+ * только на mode из write-file-atomic (его временный файл создаётся с учётом
23
+ * umask, итоговые биты могут отличаться) — после записи права принудительно
24
+ * выставляются через chmod, чтобы гарантировать ровно заданное значение
25
+ * (например 0o600 для файлов с секретами/конфигами инструментов).
26
+ */
27
+ export declare function atomicWrite(filePath: string, contents: string, mode?: number): Promise<void>;
28
+ //# sourceMappingURL=fs-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-ops.d.ts","sourceRoot":"","sources":["../../src/core/fs-ops.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQtD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Формат-агностичные файловые операции: чтение, бэкап, атомарная запись.
3
+ *
4
+ * Адаптеры (JSON/YAML) работают через эти примитивы, чтобы логика
5
+ * «прочитать существующее → забэкапить → атомарно записать» была единой
6
+ * и не дублировалась в каждом адаптере.
7
+ */
8
+ import writeFileAtomic from 'write-file-atomic';
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ /**
12
+ * Читает файл как строку. Возвращает null, если файла нет.
13
+ * Бросает только на реальных ошибках ввода-вывода (права и т.п.).
14
+ */
15
+ export function readRaw(filePath) {
16
+ if (!fs.existsSync(filePath)) {
17
+ return null;
18
+ }
19
+ return fs.readFileSync(filePath, 'utf8');
20
+ }
21
+ /**
22
+ * Создаёт бэкап файла с таймстемпом, безопасным для файловых систем.
23
+ *
24
+ * Имя: `<path>.bak.<ISO-с-замещёнными-:.>`. Двоеточия и точки в ISO-метке
25
+ * заменяются на `-`, иначе имя ломается на Windows/некоторых FS.
26
+ *
27
+ * Возвращает путь к бэкапу, либо null если исходного файла нет
28
+ * (бэкапить нечего — это не ошибка).
29
+ */
30
+ export function backup(filePath) {
31
+ if (!fs.existsSync(filePath)) {
32
+ return null;
33
+ }
34
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
35
+ const backupPath = `${filePath}.bak.${ts}`;
36
+ fs.copyFileSync(filePath, backupPath);
37
+ return backupPath;
38
+ }
39
+ /**
40
+ * Атомарная запись: гарантирует существование родительского каталога,
41
+ * затем пишет через write-file-atomic (tmp + rename), чтобы файл никогда
42
+ * не оставался в полу-записанном состоянии.
43
+ *
44
+ * Опциональный `mode` задаёт права создаваемого файла. Мы НЕ полагаемся
45
+ * только на mode из write-file-atomic (его временный файл создаётся с учётом
46
+ * umask, итоговые биты могут отличаться) — после записи права принудительно
47
+ * выставляются через chmod, чтобы гарантировать ровно заданное значение
48
+ * (например 0o600 для файлов с секретами/конфигами инструментов).
49
+ */
50
+ export async function atomicWrite(filePath, contents, mode) {
51
+ const dir = path.dirname(filePath);
52
+ if (!fs.existsSync(dir)) {
53
+ fs.mkdirSync(dir, { recursive: true });
54
+ }
55
+ if (mode === undefined) {
56
+ await writeFileAtomic(filePath, contents);
57
+ return;
58
+ }
59
+ await writeFileAtomic(filePath, contents, { mode });
60
+ // Гарантируем точные биты независимо от umask на этапе создания tmp-файла.
61
+ fs.chmodSync(filePath, mode);
62
+ }
63
+ //# sourceMappingURL=fs-ops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fs-ops.js","sourceRoot":"","sources":["../../src/core/fs-ops.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,eAAe,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,QAAgB;IACtC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,MAAM,CAAC,QAAgB;IACrC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC1D,MAAM,UAAU,GAAG,GAAG,QAAQ,QAAQ,EAAE,EAAE,CAAC;IAC3C,EAAE,CAAC,YAAY,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;IACtC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,QAAgB,EAChB,QAAgB,EAChB,IAAa;IAEb,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IACnC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACxB,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,CAAC;IACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,MAAM,eAAe,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC1C,OAAO;IACT,CAAC;IACD,MAAM,eAAe,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,2EAA2E;IAC3E,EAAE,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AAC/B,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Слияния JSON-конфигов.
3
+ *
4
+ * Общий принцип всех merge: НЕ разрушать чужие поля. Пользователь мог
5
+ * настроить инструмент под себя (хуки, лимиты, другие провайдеры) — мы
6
+ * трогаем только те ключи, которые относятся к переключению на JoinGonka
7
+ * Gateway.
8
+ */
9
+ /** Произвольный JSON-объект конфига (частичная форма — чужие поля сохраняются). */
10
+ export type JsonObject = Record<string, unknown>;
11
+ /**
12
+ * Слияние JSON-конфигов на ОДИН уровень вложенности по `env`.
13
+ *
14
+ * - Все top-level поля existing сохраняются.
15
+ * - patch перезаписывает совпадающие top-level ключи ЦЕЛИКОМ, КРОМЕ `env`:
16
+ * блок `env` сливается по ключам (deep-merge на один уровень), чтобы
17
+ * не потерять чужие переменные окружения (CUSTOM_VAR, EDITOR и т.п.).
18
+ *
19
+ * Глубокий merge ограничен `env` намеренно: для Claude Code это единственный
20
+ * вложенный объект, куда мы дописываем свои ключи рядом с пользовательскими.
21
+ * Для произвольно-вложенных схем (OpenClaw) используется deepMergeJson.
22
+ *
23
+ * @param existing текущий объект конфига (null/массив/примитив → пустой)
24
+ * @param patch наши поля для записи
25
+ */
26
+ export declare function mergeJson(existing: unknown, patch: JsonObject): JsonObject;
27
+ /**
28
+ * Рекурсивный merge двух JSON-объектов (immutable: base не мутируется).
29
+ *
30
+ * Правила:
31
+ * - оба значения по ключу — plain-объекты → сливаем рекурсивно;
32
+ * - значение из patch === undefined → НЕ затирает существующее (пропускаем);
33
+ * - иначе значение из patch заменяет значение base целиком (массивы и
34
+ * примитивы НЕ сливаются — для конфигов «заменить список» предсказуемее,
35
+ * чем конкатенация дублей; точечный upsert элементов делает upsertById).
36
+ *
37
+ * Применяется к глубоко-вложенным схемам (OpenClaw: models.providers.*,
38
+ * agents.defaults.*), где чужие ветки (другой провайдер, чужие алиасы)
39
+ * должны пережить наш upsert.
40
+ *
41
+ * @param base текущий объект (null/массив/примитив → пустой объект)
42
+ * @param patch объект с нашими полями
43
+ */
44
+ export declare function deepMergeJson(base: unknown, patch: JsonObject): JsonObject;
45
+ /**
46
+ * Upsert элемента в массив по полю `id` (immutable: исходный массив не
47
+ * мутируется, возвращается новая копия).
48
+ *
49
+ * - запись с таким `id` есть → обновляется (поля item накладываются поверх,
50
+ * чужие поля существующей записи сохраняются — shallow-merge);
51
+ * - записи нет → item добавляется в конец.
52
+ *
53
+ * Идемпотентность: повторный upsert той же записи не плодит дубликаты.
54
+ * Не-объектные элементы исходного массива пропускаются нетронутыми.
55
+ *
56
+ * @param array исходный массив записей (произвольных)
57
+ * @param item запись с обязательным полем id
58
+ */
59
+ export declare function upsertById(array: unknown[], item: JsonObject & {
60
+ id: string;
61
+ }): unknown[];
62
+ //# sourceMappingURL=merge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../src/core/merge.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,mFAAmF;AACnF,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAOjD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAa1E;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,GAAG,UAAU,CAiB1E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,UAAU,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,EAAE,CAUzF"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Слияния JSON-конфигов.
3
+ *
4
+ * Общий принцип всех merge: НЕ разрушать чужие поля. Пользователь мог
5
+ * настроить инструмент под себя (хуки, лимиты, другие провайдеры) — мы
6
+ * трогаем только те ключи, которые относятся к переключению на JoinGonka
7
+ * Gateway.
8
+ */
9
+ /** true для plain-объекта (не null, не массив). */
10
+ function isPlainObject(value) {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+ /**
14
+ * Слияние JSON-конфигов на ОДИН уровень вложенности по `env`.
15
+ *
16
+ * - Все top-level поля existing сохраняются.
17
+ * - patch перезаписывает совпадающие top-level ключи ЦЕЛИКОМ, КРОМЕ `env`:
18
+ * блок `env` сливается по ключам (deep-merge на один уровень), чтобы
19
+ * не потерять чужие переменные окружения (CUSTOM_VAR, EDITOR и т.п.).
20
+ *
21
+ * Глубокий merge ограничен `env` намеренно: для Claude Code это единственный
22
+ * вложенный объект, куда мы дописываем свои ключи рядом с пользовательскими.
23
+ * Для произвольно-вложенных схем (OpenClaw) используется deepMergeJson.
24
+ *
25
+ * @param existing текущий объект конфига (null/массив/примитив → пустой)
26
+ * @param patch наши поля для записи
27
+ */
28
+ export function mergeJson(existing, patch) {
29
+ // Защита от null / массивов / примитивов — конфиг должен быть объектом
30
+ const base = isPlainObject(existing) ? existing : {};
31
+ const merged = { ...base, ...patch };
32
+ // env сливаем по ключам, а не заменяем целиком
33
+ if (isPlainObject(patch.env)) {
34
+ const existingEnv = isPlainObject(base.env) ? base.env : {};
35
+ merged.env = { ...existingEnv, ...patch.env };
36
+ }
37
+ return merged;
38
+ }
39
+ /**
40
+ * Рекурсивный merge двух JSON-объектов (immutable: base не мутируется).
41
+ *
42
+ * Правила:
43
+ * - оба значения по ключу — plain-объекты → сливаем рекурсивно;
44
+ * - значение из patch === undefined → НЕ затирает существующее (пропускаем);
45
+ * - иначе значение из patch заменяет значение base целиком (массивы и
46
+ * примитивы НЕ сливаются — для конфигов «заменить список» предсказуемее,
47
+ * чем конкатенация дублей; точечный upsert элементов делает upsertById).
48
+ *
49
+ * Применяется к глубоко-вложенным схемам (OpenClaw: models.providers.*,
50
+ * agents.defaults.*), где чужие ветки (другой провайдер, чужие алиасы)
51
+ * должны пережить наш upsert.
52
+ *
53
+ * @param base текущий объект (null/массив/примитив → пустой объект)
54
+ * @param patch объект с нашими полями
55
+ */
56
+ export function deepMergeJson(base, patch) {
57
+ const out = isPlainObject(base) ? { ...base } : {};
58
+ for (const [key, patchValue] of Object.entries(patch)) {
59
+ if (patchValue === undefined) {
60
+ // Явный undefined в patch не должен затирать существующее значение
61
+ continue;
62
+ }
63
+ const baseValue = out[key];
64
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) {
65
+ out[key] = deepMergeJson(baseValue, patchValue);
66
+ }
67
+ else {
68
+ out[key] = patchValue;
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+ /**
74
+ * Upsert элемента в массив по полю `id` (immutable: исходный массив не
75
+ * мутируется, возвращается новая копия).
76
+ *
77
+ * - запись с таким `id` есть → обновляется (поля item накладываются поверх,
78
+ * чужие поля существующей записи сохраняются — shallow-merge);
79
+ * - записи нет → item добавляется в конец.
80
+ *
81
+ * Идемпотентность: повторный upsert той же записи не плодит дубликаты.
82
+ * Не-объектные элементы исходного массива пропускаются нетронутыми.
83
+ *
84
+ * @param array исходный массив записей (произвольных)
85
+ * @param item запись с обязательным полем id
86
+ */
87
+ export function upsertById(array, item) {
88
+ const out = [...array];
89
+ const idx = out.findIndex((entry) => isPlainObject(entry) && entry.id === item.id);
90
+ if (idx === -1) {
91
+ out.push({ ...item });
92
+ return out;
93
+ }
94
+ const existing = out[idx];
95
+ out[idx] = { ...existing, ...item };
96
+ return out;
97
+ }
98
+ //# sourceMappingURL=merge.js.map