@thesashadev/girl-agent 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,44 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.9
4
+
5
+ Дата: 2026-05-07
6
+
7
+ - Merge pull request #48 from TheSashaDev/devin/1778156776-auto-release-workflow
8
+ - Merge pull request #50 from TheSashaDev/devin/1778176335-fix-markdown-escape
9
+ - fix: also switch userbot editLastMessage to HTML spoilers, remove dead escapeMarkdownV2
10
+ - fix(telegram): replace MarkdownV2 with HTML spoilers, plain text default (#46)
11
+ - feat(ci): auto-release workflow — hourly patch bump + changelog
12
+ - Merge pull request #47 from TheSashaDev/devin/1778156514-docker-latest-on-master
13
+ - fix(docker): tag latest on master pushes, not main
14
+ - Merge pull request #45 from TheSashaDev/devin/1778149966-fix-dockerfile-build-stage
15
+ - fix(docker): add build tools to build stage for arm64 native modules
16
+ - Merge pull request #44 from TheSashaDev/devin/1778149678-fix-dockerfile-arm64
17
+ - fix(docker): add build tools for native modules on alpine arm64
18
+ - Merge pull request #43 from TheSashaDev/devin/1778149272-fix-docker-install
19
+ - fix: docker install — fix branch ref (main→master), fallback to local on pull failure
20
+ - Merge pull request #37 from TheSashaDev/devin/1778090781-windows-installer-webui
21
+ - feat(server): curl|sh installer + docker image + headless server mode
22
+ - fix(cli): fail loudly on non-TTY terminals + catch unhandled rejections
23
+ - feat(installer/desktop): paste, ClaudeHub referral, tournament names, custom sleep, profile picker
24
+ - fix(installer): silent crash on Windows — panic=abort + windowed subsystem hid the panic
25
+ - fix(installer): replace empty-text widgets in progress header with Space
26
+ - feat(installer): bundle portable Node + cli.js, full TS-wizard parity, Cyrillic fonts
27
+ - perf: add release-fast profile, mold linker, windows_subsystem
28
+ - Добавить ссылки на Telegram канал и сообщество
29
+ - feat: native Windows installer + desktop app + web UI (Rust/iced)
30
+ - Merge pull request #36 from TheSashaDev/devin/1778089384-changelog-pr35
31
+ - docs: add PR 35 to changelog
32
+
33
+ ## 0.1.8 — OpenAI-compatible API compatibility
34
+
35
+ Дата: 2026-05-06
36
+
37
+ - JSON-ответы теперь сначала запрашиваются через `json_schema`, с fallback на `json_object` и `text` для разных OpenAI-compatible API. (#33)
38
+ - LM Studio и Ollama больше не требуют реальный API ключ в wizard/headless setup.
39
+ - Добавлена совместимость с OpenAI-compatible прокси, которые возвращают SSE/event-stream даже на обычный chat completions запрос.
40
+ - Добавлена Docker-поддержка для 24/7 запуска на сервере: `Dockerfile`, `docker-compose.yml`, volume для `data` и инструкции в README. (#35)
41
+
3
42
  ## 0.1.7 — MarkdownV2 escaping fix
4
43
 
5
44
  Дата: 2026-05-06
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ![girl-agent banner](https://girl-agent.com/og-image.png)
1
+ ![girl-agent banner](https://girl-agent.com/og-image.png)
2
2
 
3
3
  [website]: https://girl-agent.com
4
4
  [docs]: https://docs.girl-agent.com
@@ -10,6 +10,8 @@
10
10
  Со всеми проблемами и багами пишите в Issues.
11
11
  ТГ создателя - @voided_net
12
12
 
13
+ Тг канал: https://t.me/GirlAgentAI/
14
+ Тг сообщество: https://t.me/GirlAgentAI_chat/
13
15
  ---
14
16
 
15
17
  ## Содержание
@@ -35,20 +37,98 @@
35
37
 
36
38
  ## Быстрый старт
37
39
 
38
- **Через NPX (рекомендуется):**
40
+ ### linux / macos / wsl — одной командой (без node на машине)
39
41
 
40
- ```powershell
41
- npx @thesashadev/girl-agent
42
+ ```sh
43
+ curl -fsSL https://raw.githubusercontent.com/TheSashaDev/girl-agent/master/scripts/install.sh | sh
44
+ ```
45
+
46
+ Что произойдёт:
47
+ - определит OS + arch (linux x64/arm64, macos x64/arm64, wsl)
48
+ - если есть docker → поставит docker-обёртку (полная изоляция от системы)
49
+ - иначе → скачает [official Node.js 22 LTS](https://nodejs.org) в `~/.local/share/girl-agent/runtime/` и поставит туда же `@thesashadev/girl-agent` (system node не трогается)
50
+ - shim-скрипт `girl-agent` положит в `~/.local/bin/girl-agent`
51
+ - ничего не пишется в `/usr/local/`, `sudo` не нужен
52
+
53
+ Дальше:
54
+ ```sh
55
+ girl-agent # ink-визард для интерактивной первичной настройки
56
+ girl-agent --profile=arina # запустить готовый профиль
57
+ girl-agent server --help # серверный режим (без TTY, для systemd / cron / CI)
58
+ ```
59
+
60
+ Опции установщика:
61
+ ```sh
62
+ # форсировать docker
63
+ curl -fsSL .../install.sh | sh -s -- --docker
64
+
65
+ # форсировать локальную ноду
66
+ curl -fsSL .../install.sh | sh -s -- --local
67
+
68
+ # конкретная версия пакета
69
+ curl -fsSL .../install.sh | sh -s -- --version=0.1.9
42
70
  ```
43
71
 
44
- Wizard задаст пару вопросов — имя, возраст, Telegram-подключение, LLM-ключ. Всё.
72
+ Удаление: `rm -rf ~/.local/share/girl-agent ~/.local/bin/girl-agent`
73
+
74
+ ### windows — десктоп-приложение
45
75
 
46
- Если профиль уже есть:
76
+ В папке `desktop-rs/` лежит нативный десктоп-клиент на Rust (iced) и инсталлер-визард: ставит Node-пакет, создаёт профиль, открывает дашборд. Параллельно поднимается локальный веб-UI на `http://127.0.0.1:7777` с тем же дашбордом — открыть из соседнего окна / телефона по локалке. Без WebView, без Electron.
47
77
 
48
78
  ```powershell
79
+ cd desktop-rs
80
+ cargo run -p girl-agent-installer # визард настройки персоны
81
+ cargo run -p girl-agent-desktop # открыть дашборд
82
+ ```
83
+
84
+ Готовые бинари будут собираться в CI чуть позже — пока нужно `cargo build --release`.
85
+
86
+ ### если уже есть node ≥ 20
87
+
88
+ ```sh
89
+ npx @thesashadev/girl-agent # ink-визард
49
90
  npx @thesashadev/girl-agent --profile=arina
50
91
  ```
51
92
 
93
+ ### docker (для серверов; нулевые зависимости на хосте)
94
+
95
+ Интерактивная первичная настройка (ink-визард внутри контейнера):
96
+ ```sh
97
+ docker run -it --rm -v girl-agent-data:/data ghcr.io/thesashadev/girl-agent:latest
98
+ ```
99
+
100
+ Headless (для systemd / docker compose / k8s) — сначала готовим конфиг, потом запускаем без TTY:
101
+ ```sh
102
+ # 1) шаблон конфига
103
+ docker run --rm ghcr.io/thesashadev/girl-agent:latest server --print-config > bot.json
104
+ # 2) отредактировать bot.json (token, api-key)
105
+ # 3) поднять в фоне
106
+ docker run -d --name girl-agent --restart=unless-stopped \
107
+ -v girl-agent-data:/data \
108
+ -v $PWD/bot.json:/config/bot.json:ro \
109
+ ghcr.io/thesashadev/girl-agent:latest \
110
+ server --config /config/bot.json --headless
111
+ ```
112
+
113
+ Или совсем без файла, через env-vars (k8s secrets, docker compose):
114
+ ```sh
115
+ docker run -d --name girl-agent --restart=unless-stopped \
116
+ -v girl-agent-data:/data \
117
+ -e GIRL_AGENT_MODE=bot \
118
+ -e GIRL_AGENT_TOKEN=... \
119
+ -e GIRL_AGENT_API_PRESET=claudehub \
120
+ -e GIRL_AGENT_API_KEY=... \
121
+ -e GIRL_AGENT_NAME='Аня' -e GIRL_AGENT_AGE=22 \
122
+ ghcr.io/thesashadev/girl-agent:latest \
123
+ server --headless
124
+ ```
125
+
126
+ Готовые шаблоны:
127
+ - `girl-agent server --print-config` — bot.json
128
+ - `girl-agent server --print-systemd` — `/etc/systemd/system/girl-agent.service`
129
+ - `girl-agent server --print-docker` — Dockerfile / compose / k8s snippets
130
+ - [`docker-compose.example.yml`](./docker-compose.example.yml) в корне репо
131
+
52
132
  **Из исходников:**
53
133
 
54
134
  ```powershell
package/dist/cli.js CHANGED
@@ -19,15 +19,19 @@ var init_esm_shims = __esm({
19
19
  });
20
20
 
21
21
  // src/telegram/markdown.ts
22
- function escapeMarkdownV2(text) {
23
- return text.replace(MD2_RESERVED, "\\$1");
22
+ function hasSpoilers(text) {
23
+ return /\|\|.+?\|\|/.test(text);
24
+ }
25
+ function escapeHtml(text) {
26
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
27
+ }
28
+ function toHtmlWithSpoilers(text) {
29
+ return escapeHtml(text).replace(/\|\|(.+?)\|\|/g, "<tg-spoiler>$1</tg-spoiler>");
24
30
  }
25
- var MD2_RESERVED;
26
31
  var init_markdown = __esm({
27
32
  "src/telegram/markdown.ts"() {
28
33
  "use strict";
29
34
  init_esm_shims();
30
- MD2_RESERVED = /([_*\[\]()~`>#+\-=|{}.!\\])/g;
31
35
  }
32
36
  });
33
37
 
@@ -190,11 +194,14 @@ function makeUserbotAdapter(cfg) {
190
194
  },
191
195
  async editLastMessage(chatId, messageId, text) {
192
196
  const peer = await resolvePeer(chatId);
193
- try {
194
- await client.editMessage(peer, { message: messageId, text: escapeMarkdownV2(text), parseMode: "MarkdownV2" });
195
- } catch {
196
- await client.editMessage(peer, { message: messageId, text });
197
+ if (hasSpoilers(text)) {
198
+ try {
199
+ await client.editMessage(peer, { message: messageId, text: toHtmlWithSpoilers(text), parseMode: "html" });
200
+ return;
201
+ } catch {
202
+ }
197
203
  }
204
+ await client.editMessage(peer, { message: messageId, text });
198
205
  },
199
206
  async deleteMessages(chatId, messageIds, revoke = false) {
200
207
  const peer = await resolvePeer(chatId);
@@ -293,13 +300,15 @@ function makeBotAdapter(cfg) {
293
300
  });
294
301
  },
295
302
  async sendText(chatId, text) {
296
- try {
297
- const msg = await bot.api.sendMessage(chatId, escapeMarkdownV2(text), { parse_mode: "MarkdownV2" });
298
- return msg.message_id;
299
- } catch {
300
- const msg = await bot.api.sendMessage(chatId, text);
301
- return msg.message_id;
303
+ if (hasSpoilers(text)) {
304
+ try {
305
+ const msg2 = await bot.api.sendMessage(chatId, toHtmlWithSpoilers(text), { parse_mode: "HTML" });
306
+ return msg2.message_id;
307
+ } catch {
308
+ }
302
309
  }
310
+ const msg = await bot.api.sendMessage(chatId, text);
311
+ return msg.message_id;
303
312
  },
304
313
  async setTyping(chatId, on) {
305
314
  if (on) {
@@ -377,8 +386,10 @@ var LLM_PRESETS = [
377
386
  proto: "openai",
378
387
  baseURL: "http://localhost:1234/v1",
379
388
  defaultModel: "",
389
+ defaultApiKey: "lm-studio",
390
+ apiKeyRequired: false,
380
391
  custom: true,
381
- hint: "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E, OpenAI-compatible endpoint"
392
+ hint: "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E, OpenAI-compatible endpoint; \u043A\u043B\u044E\u0447 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D"
382
393
  },
383
394
  {
384
395
  id: "ollama",
@@ -386,8 +397,10 @@ var LLM_PRESETS = [
386
397
  proto: "openai",
387
398
  baseURL: "http://localhost:11434/v1",
388
399
  defaultModel: "llama3.1",
400
+ defaultApiKey: "ollama",
401
+ apiKeyRequired: false,
389
402
  custom: true,
390
- hint: "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u0447\u0435\u0440\u0435\u0437 /v1"
403
+ hint: "\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E \u0447\u0435\u0440\u0435\u0437 /v1; \u043A\u043B\u044E\u0447 \u043D\u0435 \u043D\u0443\u0436\u0435\u043D"
391
404
  },
392
405
  {
393
406
  id: "anthropic",
@@ -796,7 +809,7 @@ function sameProfile(a, b) {
796
809
  init_esm_shims();
797
810
  import { promises as fs } from "fs";
798
811
  import path2 from "path";
799
- var DATA_ROOT = path2.resolve(process.cwd(), "data");
812
+ var DATA_ROOT = process.env.GIRL_AGENT_DATA ? path2.resolve(process.env.GIRL_AGENT_DATA) : path2.resolve(process.cwd(), "data");
800
813
  function profileDir(slug) {
801
814
  return path2.join(DATA_ROOT, slug);
802
815
  }
@@ -1025,20 +1038,28 @@ var OpenAILike = class {
1025
1038
  constructor(cfg) {
1026
1039
  this.cfg = cfg;
1027
1040
  this.client = new OpenAI({
1028
- apiKey: cfg.apiKey,
1041
+ apiKey: openAIApiKey(cfg),
1029
1042
  baseURL: normalizeBaseURL(cfg.baseURL),
1030
1043
  timeout: LLM_TIMEOUT_MS,
1031
1044
  maxRetries: LLM_MAX_RETRIES
1032
1045
  });
1046
+ this.fetchClient = new OpenAI({
1047
+ apiKey: openAIApiKey(cfg),
1048
+ baseURL: normalizeBaseURL(cfg.baseURL),
1049
+ timeout: LLM_TIMEOUT_MS,
1050
+ maxRetries: LLM_MAX_RETRIES,
1051
+ fetch: compatibleFetch
1052
+ });
1033
1053
  }
1034
1054
  cfg;
1035
1055
  client;
1056
+ fetchClient;
1036
1057
  async chat(messages, opts = {}) {
1037
1058
  const params = {
1038
1059
  model: this.cfg.model,
1039
1060
  messages: openAIMessages(messages),
1040
1061
  temperature: opts.temperature ?? 0.85,
1041
- response_format: opts.json ? { type: "json_object" } : void 0
1062
+ response_format: openAIResponseFormat(opts)
1042
1063
  };
1043
1064
  if (usesMaxCompletionTokens(this.cfg.model)) {
1044
1065
  params.max_completion_tokens = opts.maxTokens ?? 600;
@@ -1049,19 +1070,86 @@ var OpenAILike = class {
1049
1070
  return res.choices[0]?.message?.content?.trim() ?? "";
1050
1071
  }
1051
1072
  async createWithCompatibilityFallback(params) {
1052
- try {
1053
- return await this.client.chat.completions.create(params);
1054
- } catch (error) {
1055
- const fallback = completionTokenFallback(params, error);
1056
- if (!fallback) throw enrichOpenAIError(error, this.cfg.baseURL);
1073
+ const attempted = /* @__PURE__ */ new Set();
1074
+ let current = params;
1075
+ let lastError;
1076
+ while (current) {
1077
+ const key = completionParamsKey(current);
1078
+ if (attempted.has(key)) break;
1079
+ attempted.add(key);
1080
+ try {
1081
+ return await this.client.chat.completions.create(current);
1082
+ } catch (error) {
1083
+ lastError = error;
1084
+ const next = completionFallback(current, error);
1085
+ if (!next) break;
1086
+ current = next;
1087
+ }
1088
+ }
1089
+ if (this.cfg.baseURL) {
1057
1090
  try {
1058
- return await this.client.chat.completions.create(fallback);
1059
- } catch (fallbackError) {
1060
- throw enrichOpenAIError(fallbackError, this.cfg.baseURL);
1091
+ return await this.fetchClient.chat.completions.create({ ...params, stream: false });
1092
+ } catch (fetchError) {
1093
+ lastError = fetchError;
1061
1094
  }
1062
1095
  }
1096
+ throw enrichOpenAIError(lastError, this.cfg.baseURL);
1063
1097
  }
1064
1098
  };
1099
+ async function compatibleFetch(url, init) {
1100
+ const res = await fetch(url, init);
1101
+ const contentType = res.headers.get("content-type") ?? "";
1102
+ if (!res.ok) return res;
1103
+ const text = await res.clone().text();
1104
+ if (contentType.includes("text/event-stream") || text.trimStart().startsWith("data:")) {
1105
+ return completionStreamToJsonResponse(res, text);
1106
+ }
1107
+ return res;
1108
+ }
1109
+ function completionStreamToJsonResponse(res, text) {
1110
+ const completion = parseOpenAIEventStream(text);
1111
+ return new Response(JSON.stringify(completion), {
1112
+ status: res.status,
1113
+ statusText: res.statusText,
1114
+ headers: { "content-type": "application/json" }
1115
+ });
1116
+ }
1117
+ function parseOpenAIEventStream(raw) {
1118
+ let id = "chatcmpl-stream";
1119
+ let model = "";
1120
+ let created = Math.floor(Date.now() / 1e3);
1121
+ const content = [];
1122
+ let finishReason = "stop";
1123
+ for (const line of raw.split(/\r?\n/)) {
1124
+ const trimmed = line.trim();
1125
+ if (!trimmed.startsWith("data:")) continue;
1126
+ const data = trimmed.slice(5).trim();
1127
+ if (!data || data === "[DONE]") continue;
1128
+ try {
1129
+ const chunk = JSON.parse(data);
1130
+ id = chunk.id || id;
1131
+ model = chunk.model || model;
1132
+ created = chunk.created || created;
1133
+ const choice = chunk.choices[0];
1134
+ finishReason = choice?.finish_reason ?? finishReason;
1135
+ const delta = choice?.delta?.content ?? choice?.message?.content;
1136
+ if (typeof delta === "string") content.push(delta);
1137
+ } catch {
1138
+ }
1139
+ }
1140
+ return {
1141
+ id,
1142
+ object: "chat.completion",
1143
+ created,
1144
+ model,
1145
+ choices: [{
1146
+ index: 0,
1147
+ message: { role: "assistant", content: content.join(""), refusal: null },
1148
+ finish_reason: finishReason,
1149
+ logprobs: null
1150
+ }]
1151
+ };
1152
+ }
1065
1153
  var AnthropicLike = class {
1066
1154
  constructor(cfg) {
1067
1155
  this.cfg = cfg;
@@ -1155,8 +1243,36 @@ function normalizeBaseURL(value) {
1155
1243
  function usesMaxCompletionTokens(model) {
1156
1244
  return /^(?:o\d|o\d-|o\d\b|gpt-5|gpt-5\.|gpt-[5-9])|\/(?:o\d|gpt-5|gpt-[5-9])/.test(model.trim().toLowerCase());
1157
1245
  }
1246
+ function openAIApiKey(cfg) {
1247
+ return cfg.apiKey.trim() || (cfg.presetId === "ollama" ? "ollama" : cfg.presetId === "lmstudio" ? "lm-studio" : "");
1248
+ }
1249
+ function openAIResponseFormat(opts) {
1250
+ if (!opts.json) return void 0;
1251
+ if (opts.jsonSchema) return { type: "json_schema", json_schema: opts.jsonSchema };
1252
+ return {
1253
+ type: "json_schema",
1254
+ json_schema: {
1255
+ name: "json_response",
1256
+ strict: false,
1257
+ schema: { type: "object", additionalProperties: true }
1258
+ }
1259
+ };
1260
+ }
1261
+ function completionFallback(params, error) {
1262
+ return responseFormatFallback(params, error) ?? completionTokenFallback(params, error);
1263
+ }
1264
+ function responseFormatFallback(params, error) {
1265
+ const message = openAIErrorText(error);
1266
+ if (!params.response_format || !message.includes("response_format")) return null;
1267
+ if (params.response_format.type === "json_schema" && message.includes("json_object")) {
1268
+ return { ...params, response_format: { type: "text" } };
1269
+ }
1270
+ if (params.response_format.type === "json_schema") return { ...params, response_format: { type: "json_object" } };
1271
+ if (params.response_format.type === "json_object") return { ...params, response_format: { type: "text" } };
1272
+ return null;
1273
+ }
1158
1274
  function completionTokenFallback(params, error) {
1159
- const message = errorMessage(error).toLowerCase();
1275
+ const message = openAIErrorText(error);
1160
1276
  if (params.max_tokens != null && message.includes("max_tokens") && message.includes("max_completion_tokens")) {
1161
1277
  const { max_tokens, ...rest } = params;
1162
1278
  return { ...rest, max_completion_tokens: max_tokens };
@@ -1167,6 +1283,16 @@ function completionTokenFallback(params, error) {
1167
1283
  }
1168
1284
  return null;
1169
1285
  }
1286
+ function completionParamsKey(params) {
1287
+ const tokenKey = params.max_completion_tokens != null ? "max_completion_tokens" : "max_tokens";
1288
+ return `${params.response_format?.type ?? "default"}:${tokenKey}`;
1289
+ }
1290
+ function openAIErrorText(error) {
1291
+ if (error instanceof OpenAI.APIError) {
1292
+ return `${error.status ?? ""} ${error.code ?? ""} ${error.type ?? ""} ${error.message}`.toLowerCase();
1293
+ }
1294
+ return errorMessage(error).toLowerCase();
1295
+ }
1170
1296
  function enrichOpenAIError(error, baseURL) {
1171
1297
  if (error instanceof OpenAI.APIConnectionError) {
1172
1298
  return new Error(connectionErrorMessage("OpenAI-compatible", baseURL, error));
@@ -1202,6 +1328,40 @@ function makeLLM(cfg) {
1202
1328
  init_esm_shims();
1203
1329
  var SYS = `\u0422\u044B \u2014 \u0440\u0435\u0436\u0438\u0441\u0441\u0451\u0440 \u043F\u0435\u0440\u0441\u043E\u043D\u0430\u0436\u0435\u0439. \u0422\u0432\u043E\u044F \u0437\u0430\u0434\u0430\u0447\u0430 \u2014 \u0441\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u043E\u0441\u0442\u043E\u0432\u0435\u0440\u043D\u044B\u0439, \u043D\u0435-\u0433\u043E\u043B\u043B\u0438\u0432\u0443\u0434\u0441\u043A\u0438\u0439, \u043D\u0435-\u0441\u043B\u0430\u0434\u043A\u0438\u0439, \u043D\u0435-AI-\u043F\u043E\u043C\u043E\u0449\u043D\u0438\u043A\u043E\u0432\u044B\u0439 \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u043E\u0431\u044B\u0447\u043D\u043E\u0439 \u0440\u0443\u0441\u0441\u043A\u043E\u044F\u0437\u044B\u0447\u043D\u043E\u0439 \u0434\u0435\u0432\u0443\u0448\u043A\u0438 \u0443\u043A\u0430\u0437\u0430\u043D\u043D\u043E\u0433\u043E \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0430. \u0411\u0435\u0437 \u043F\u0440\u0438\u0443\u043A\u0440\u0430\u0448\u0438\u0432\u0430\u043D\u0438\u0439, \u0431\u0435\u0437 \u043F\u043E\u0437\u0438\u0442\u0438\u0432\u043D\u043E\u0433\u043E \u043C\u043E\u0440\u0430\u043B\u044C\u043D\u043E\u0433\u043E \u0443\u043A\u043B\u043E\u043D\u0430, \u0431\u0435\u0437 \xAB\u0443\u0434\u043E\u0431\u043D\u043E\u0439\xBB \u043F\u0435\u0440\u0441\u043E\u043D\u044B. \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u0442\u043A\u0438, \u0443\u0437\u043A\u0438\u0435 \u0438\u043D\u0442\u0435\u0440\u0435\u0441\u044B, \u0441\u043A\u0435\u043F\u0441\u0438\u0441, \u0442\u043E\u043A\u0441\u0438\u0447\u043D\u043E\u0441\u0442\u044C, \u043B\u0435\u043D\u044C, \u0442\u0440\u0435\u0432\u043E\u0436\u043D\u043E\u0441\u0442\u044C, \u043D\u0435\u0443\u0432\u0435\u0440\u0435\u043D\u043D\u043E\u0441\u0442\u044C \u2014 \u0447\u0442\u043E \u043F\u043E\u0434\u0445\u043E\u0434\u0438\u0442 \u0432\u043E\u0437\u0440\u0430\u0441\u0442\u0443. \u041D\u0438\u043A\u0430\u043A\u0438\u0445 \xAB\u043A\u043E\u0443\u0447\u0438\u043D\u0433\u043E\u0432\u044B\u0445\xBB \u0444\u043E\u0440\u043C\u0443\u043B\u0438\u0440\u043E\u0432\u043E\u043A. \u0422\u043E\u043B\u044C\u043A\u043E \u0436\u0438\u0432\u0430\u044F \u0440\u0435\u0447\u044C, \u043A\u0430\u043A \u0438\u0437 \u0434\u043D\u0435\u0432\u043D\u0438\u043A\u0430 \u0438\u043B\u0438 \u0432\u043D\u0443\u0442\u0440\u0435\u043D\u043D\u0435\u0433\u043E \u043C\u043E\u043D\u043E\u043B\u043E\u0433\u0430. \u0412\u043E\u0437\u0440\u0430\u0441\u0442: {{age}} \u043B\u0435\u0442, \u0438\u043C\u044F: {{name}}.`;
1204
1330
  var WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"];
1331
+ var BUSY_SCHEDULE_SCHEMA = {
1332
+ name: "busy_schedule",
1333
+ strict: false,
1334
+ schema: {
1335
+ type: "object",
1336
+ properties: {
1337
+ busySchedule: {
1338
+ type: "array",
1339
+ items: {
1340
+ type: "object",
1341
+ properties: {
1342
+ label: { type: "string" },
1343
+ days: {
1344
+ type: "array",
1345
+ items: { type: "string", enum: WEEKDAYS }
1346
+ },
1347
+ from: { type: "string" },
1348
+ to: { type: "string" },
1349
+ checkAfterMin: {
1350
+ type: "array",
1351
+ items: { type: "number" },
1352
+ minItems: 2,
1353
+ maxItems: 2
1354
+ }
1355
+ },
1356
+ required: ["label", "from", "to"],
1357
+ additionalProperties: false
1358
+ }
1359
+ }
1360
+ },
1361
+ required: ["busySchedule"],
1362
+ additionalProperties: false
1363
+ }
1364
+ };
1205
1365
  async function generatePersonaPack(llm, slug, name, age, nationality = "RU", personaNotes = "", onProgress) {
1206
1366
  const country = nationality === "UA" ? "\u0423\u043A\u0440\u0430\u0438\u043D\u0430" : "\u0420\u043E\u0441\u0441\u0438\u044F / \u0421\u041D\u0413";
1207
1367
  const langHint = nationality === "UA" ? "\u041F\u0438\u0448\u0435\u0442 \u043D\u0430 \u0420\u0423\u0421\u0421\u041A\u041E\u041C (\u043A\u0430\u043A \u0440\u0435\u0430\u043B\u044C\u043D\u043E \u043F\u0438\u0448\u0435\u0442 \u0431\u043E\u043B\u044C\u0448\u0438\u043D\u0441\u0442\u0432\u043E \u0434\u0435\u0432\u0443\u0448\u0435\u043A \u0432 \u0423\u043A\u0440\u0430\u0438\u043D\u0435 \u0432 \u0442\u0433). \u0414\u043E\u043F\u0443\u0441\u0442\u0438\u043C \u043B\u0451\u0433\u043A\u0438\u0439 \u0441\u0443\u0440\u0436\u0438\u043A: ~90% \u0440\u0443\u0441\u0441\u043A\u0438\u0439 + ~10% \u0443\u043A\u0440\u0430\u0438\u043D\u0441\u043A\u0438\u0445 \u0432\u0441\u0442\u0430\u0432\u043E\u043A (\u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0435 \u0441\u043B\u043E\u0432\u0430: '\u0448\u043E', '\u043C\u0430\u0431\u0443\u0442\u044C', '\u0442\u0440\u043E\u0445\u0438', '\u044F\u043A', '\u043D\u0443 \u0448\u043E', '\u0442\u0430 \u0439\u0434\u0438', '\u0434\u044F\u043A\u0443\u044E'), \u043D\u043E \u0431\u0435\u0437 \u043F\u043E\u043B\u043D\u043E\u0433\u043E \u043F\u0435\u0440\u0435\u0445\u043E\u0434\u0430 \u043D\u0430 \u0443\u043A\u0440\u0430\u0438\u043D\u0441\u043A\u0438\u0439. \u0427\u0438\u0441\u0442\u043E-\u0443\u043A\u0440\u0430\u0438\u043D\u0441\u043A\u0438\u0439 \u0442\u0435\u043A\u0441\u0442 \u041D\u0415 \u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0439 \u2014 \u044D\u0442\u043E \u043D\u0435\u0440\u0435\u0430\u043B\u0438\u0441\u0442\u0438\u0447\u043D\u043E \u0434\u043B\u044F \u0442\u0433-\u043F\u0435\u0440\u0435\u043F\u0438\u0441\u043A\u0438." : "\u0420\u0443\u0441\u0441\u043A\u043E\u044F\u0437\u044B\u0447\u043D\u0430\u044F \u0431\u0435\u0437 \u0443\u043A\u0440\u0430\u0438\u043D\u0438\u0437\u043C\u043E\u0432.";
@@ -1312,7 +1472,7 @@ ${personaNotes.trim()}
1312
1472
  onProgress?.(65, "\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C communication.md\u2026");
1313
1473
  const boundaries = await llm.chat([{ role: "system", content: sys }, { role: "user", content: boundariesPrompt }], { temperature: 0.9, maxTokens: 3500 });
1314
1474
  onProgress?.(85, "\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C busy schedule\u2026");
1315
- const routineRaw = await llm.chat([{ role: "system", content: sys }, { role: "user", content: routinePrompt }], { temperature: 0.85, maxTokens: 3500, json: true });
1475
+ const routineRaw = await llm.chat([{ role: "system", content: sys }, { role: "user", content: routinePrompt }], { temperature: 0.85, maxTokens: 3500, json: true, jsonSchema: BUSY_SCHEDULE_SCHEMA });
1316
1476
  const busySchedule = parseBusySchedule(routineRaw, name, age);
1317
1477
  await writeMd(slug, "persona.md", persona);
1318
1478
  await writeMd(slug, "speech.md", speech);
@@ -1383,14 +1543,14 @@ var DEFAULT_PROXY = "https://tgproxy.girl-agent.com";
1383
1543
  function proxyUrl() {
1384
1544
  return process.env.GIRL_AGENT_AUTH_PROXY ?? DEFAULT_PROXY;
1385
1545
  }
1386
- async function post(path5, body) {
1387
- const res = await fetch(`${proxyUrl()}${path5}`, {
1546
+ async function post(path6, body) {
1547
+ const res = await fetch(`${proxyUrl()}${path6}`, {
1388
1548
  method: "POST",
1389
1549
  headers: { "Content-Type": "application/json" },
1390
1550
  body: JSON.stringify(body)
1391
1551
  });
1392
1552
  const data = await res.json();
1393
- if (!res.ok) throw new Error(data.error ?? `proxy ${path5} failed (${res.status})`);
1553
+ if (!res.ok) throw new Error(data.error ?? `proxy ${path6} failed (${res.status})`);
1394
1554
  return data;
1395
1555
  }
1396
1556
  function remoteSendCode(phone) {
@@ -1866,6 +2026,7 @@ function Wizard({ initial, onDone }) {
1866
2026
  setLlmProto(preset.proto);
1867
2027
  setLlmBaseURL(preset.baseURL ?? "");
1868
2028
  setLlmModel(preset.defaultModel);
2029
+ setLlmKey(preset.defaultApiKey ?? "");
1869
2030
  if (preset.custom) setStep("api-base");
1870
2031
  else setStep("api-model");
1871
2032
  }
@@ -1893,7 +2054,9 @@ function Wizard({ initial, onDone }) {
1893
2054
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435 \u043C\u043E\u0434\u0435\u043B\u0438" }), /* @__PURE__ */ React.createElement(Bar, { step: 2, total: 9 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Model: "), /* @__PURE__ */ React.createElement(TextInput, { value: llmModel, onChange: setLlmModel, onSubmit: () => setStep("api-key") })));
1894
2055
  }
1895
2056
  if (step === "api-key") {
1896
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "API \u043A\u043B\u044E\u0447" }), /* @__PURE__ */ React.createElement(Bar, { step: 2, total: 11 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Key: "), /* @__PURE__ */ React.createElement(TextInput, { value: llmKey, onChange: setLlmKey, mask: "\u2022", onSubmit: () => llmKey && setStep("nationality") })));
2057
+ const preset = findPreset(llmPresetId);
2058
+ const apiKeyRequired = preset?.apiKeyRequired !== false;
2059
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: apiKeyRequired ? "API \u043A\u043B\u044E\u0447" : "API \u043A\u043B\u044E\u0447 (\u043C\u043E\u0436\u043D\u043E \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u0442\u044C)" }), /* @__PURE__ */ React.createElement(Bar, { step: 2, total: 11 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(Text, null, "Key: "), /* @__PURE__ */ React.createElement(TextInput, { value: llmKey, onChange: setLlmKey, mask: "\u2022", onSubmit: () => (llmKey || !apiKeyRequired) && setStep("nationality") })), !apiKeyRequired && /* @__PURE__ */ React.createElement(Text, { dimColor: true }, "\u0414\u043B\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u043E\u0433\u043E API \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0442\u0435\u0445\u043D\u0438\u0447\u0435\u0441\u043A\u0438\u0439 placeholder, \u0435\u0441\u043B\u0438 \u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u043F\u0443\u0441\u0442\u043E."));
1897
2060
  }
1898
2061
  if (step === "nationality") {
1899
2062
  return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", padding: 1 }, /* @__PURE__ */ React.createElement(Header, { sub: "\u043D\u0430\u0446\u0438\u043E\u043D\u0430\u043B\u044C\u043D\u043E\u0441\u0442\u044C (\u044F\u0437\u044B\u043A, \u0438\u043C\u044F, \u043A\u0443\u043B\u044C\u0442\u0443\u0440\u0430)" }), /* @__PURE__ */ React.createElement(Bar, { step: 3, total: 11 }), /* @__PURE__ */ React.createElement(Box, { marginTop: 1 }, /* @__PURE__ */ React.createElement(
@@ -2939,8 +3102,8 @@ phoneAvailable=false \u043A\u043E\u0433\u0434\u0430: \u0441\u043F\u0438\u0442, \
2939
3102
  }
2940
3103
  async function loadOrGenerateDailyLife(llm, cfg, now = /* @__PURE__ */ new Date(), conflict = null) {
2941
3104
  const dateLocal = localDateStr(cfg.tz, now);
2942
- const path5 = `daily-life/${dateLocal}.md`;
2943
- const existing = await readMd(cfg.slug, path5);
3105
+ const path6 = `daily-life/${dateLocal}.md`;
3106
+ const existing = await readMd(cfg.slug, path6);
2944
3107
  if (existing) {
2945
3108
  try {
2946
3109
  const m = existing.match(/<!--daily:(.+?)-->/s);
@@ -2972,7 +3135,7 @@ async function loadOrGenerateDailyLife(llm, cfg, now = /* @__PURE__ */ new Date(
2972
3135
  dl = { dateLocal, vibe: "\u043E\u0431\u044B\u0447\u043D\u044B\u0439 \u0434\u0435\u043D\u044C", blocks: [], events: [], wants: [] };
2973
3136
  }
2974
3137
  const human = renderDailyLifeHuman(dl);
2975
- await writeMd(cfg.slug, path5, `${human}
3138
+ await writeMd(cfg.slug, path6, `${human}
2976
3139
 
2977
3140
  <!--daily:${JSON.stringify(dl)}-->
2978
3141
  `);
@@ -3202,9 +3365,9 @@ async function ensureDefaults(cfg) {
3202
3365
  ["time/promises.md", "# promises\n"],
3203
3366
  ["memory/uncertain.md", "# uncertain\n"]
3204
3367
  ];
3205
- await Promise.all(defaults.map(async ([path5, content]) => {
3206
- const current = await readMd(cfg.slug, path5);
3207
- if (!current.trim()) await writeMd(cfg.slug, path5, content + "\n");
3368
+ await Promise.all(defaults.map(async ([path6, content]) => {
3369
+ const current = await readMd(cfg.slug, path6);
3370
+ if (!current.trim()) await writeMd(cfg.slug, path6, content + "\n");
3208
3371
  }));
3209
3372
  }
3210
3373
  async function loadRealismContext(cfg, incoming) {
@@ -5576,6 +5739,536 @@ function sleep(ms) {
5576
5739
  return new Promise((r) => setTimeout(r, ms));
5577
5740
  }
5578
5741
 
5742
+ // src/headless.ts
5743
+ init_esm_shims();
5744
+ import readline from "readline";
5745
+ async function runHeadlessJsonEvents(rt) {
5746
+ const out = (obj) => {
5747
+ process.stdout.write(JSON.stringify(obj) + "\n");
5748
+ };
5749
+ out({ type: "ready", profile: profileSummary(rt.cfg) });
5750
+ rt.on("event", (e) => {
5751
+ out({ ...e, t: Date.now() });
5752
+ });
5753
+ try {
5754
+ const r = await readRelationship(rt.cfg.slug);
5755
+ out({ type: "score", score: r.score, t: Date.now() });
5756
+ } catch {
5757
+ }
5758
+ let paused = false;
5759
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
5760
+ rl.on("line", async (raw) => {
5761
+ const line = raw.trim();
5762
+ if (!line) return;
5763
+ if (!line.startsWith(":")) {
5764
+ out({ type: "response", ok: false, text: "\u043A\u043E\u043C\u0430\u043D\u0434\u044B \u043D\u0430\u0447\u0438\u043D\u0430\u044E\u0442\u0441\u044F \u0441 :" });
5765
+ return;
5766
+ }
5767
+ const [head, ...rest] = line.slice(1).split(" ");
5768
+ try {
5769
+ let text = "";
5770
+ switch (head) {
5771
+ case "status":
5772
+ text = await rt.cmdStatus();
5773
+ break;
5774
+ case "reset":
5775
+ text = await rt.cmdReset();
5776
+ break;
5777
+ case "stage":
5778
+ text = await rt.cmdSetStage(rest.join(" "));
5779
+ break;
5780
+ case "wake":
5781
+ text = await rt.cmdWake(rest[0]);
5782
+ break;
5783
+ case "debug":
5784
+ text = await rt.cmdDebug(rest[0]);
5785
+ break;
5786
+ case "why":
5787
+ text = await rt.cmdWhy(rest[0]);
5788
+ break;
5789
+ case "amnesia":
5790
+ text = await rt.cmdAmnesia(rest[0], rest[1]);
5791
+ break;
5792
+ case "block":
5793
+ text = await rt.cmdBlock(rest[0]);
5794
+ break;
5795
+ case "unblock":
5796
+ text = await rt.cmdUnblock(rest[0]);
5797
+ break;
5798
+ case "read":
5799
+ text = await rt.cmdRead(rest[0]);
5800
+ break;
5801
+ case "clear-chat":
5802
+ text = await rt.cmdClearChat(rest.find((x) => !x.startsWith("--")), rest.includes("--revoke"));
5803
+ break;
5804
+ case "report-spam":
5805
+ text = await rt.cmdReportSpam(rest[0]);
5806
+ break;
5807
+ case "delete-last":
5808
+ text = await rt.cmdDeleteLast(rest.find((x) => !x.startsWith("--")), !rest.includes("--local"));
5809
+ break;
5810
+ case "edit-last":
5811
+ text = await rt.cmdEditLast(rest.join(" "));
5812
+ break;
5813
+ case "sticker":
5814
+ text = await rt.cmdSticker(rest[0]);
5815
+ break;
5816
+ case "pause":
5817
+ rt.pause();
5818
+ paused = true;
5819
+ text = "\u23F8 pause";
5820
+ break;
5821
+ case "resume":
5822
+ rt.resume();
5823
+ paused = false;
5824
+ text = "\u25B6 resume";
5825
+ break;
5826
+ case "cringe": {
5827
+ const r = await readRelationship(rt.cfg.slug);
5828
+ text = `cringe=${r.score.cringe}; \u0441\u043C. memory/long-term.md \u0438 log/`;
5829
+ break;
5830
+ }
5831
+ case "relationship": {
5832
+ const r = await readRelationship(rt.cfg.slug);
5833
+ text = `stage=${r.stage} score=${JSON.stringify(r.score)}`;
5834
+ break;
5835
+ }
5836
+ case "persona": {
5837
+ const p = await readMd(rt.cfg.slug, "persona.md");
5838
+ text = p.slice(0, 4e3);
5839
+ break;
5840
+ }
5841
+ case "log": {
5842
+ const day = /^\d{4}-\d{2}-\d{2}$/.test(rest[0] ?? "") ? rest[0] : sessionDate(rt.cfg.tz);
5843
+ const limit = Number(rest.find((x) => /^\d+$/.test(x)) ?? 3e3);
5844
+ const p = await readSessionLog(rt.cfg.slug, day);
5845
+ text = p.trim() ? p.slice(-Math.max(500, Math.min(limit, 2e4))) : `(log/${day}.md \u043F\u0443\u0441\u0442)`;
5846
+ break;
5847
+ }
5848
+ case "snapshot": {
5849
+ const r = await readRelationship(rt.cfg.slug);
5850
+ out({
5851
+ type: "snapshot",
5852
+ t: Date.now(),
5853
+ paused,
5854
+ profile: profileSummary(rt.cfg),
5855
+ stage: { id: r.stage, label: findStage(r.stage).label },
5856
+ score: r.score
5857
+ });
5858
+ return;
5859
+ }
5860
+ case "help":
5861
+ text = ":status :why :amnesia :reset :stage :wake :debug :pause :resume :cringe :relationship :persona :log :block :unblock :read :clear-chat :report-spam :delete-last :edit-last :sticker :snapshot :quit";
5862
+ break;
5863
+ case "quit":
5864
+ case "exit":
5865
+ await rt.stop();
5866
+ out({ type: "response", ok: true, text: "bye" });
5867
+ process.exit(0);
5868
+ default:
5869
+ out({ type: "response", ok: false, text: `\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043A\u043E\u043C\u0430\u043D\u0434\u0430: ${head}` });
5870
+ return;
5871
+ }
5872
+ out({ type: "response", ok: true, text });
5873
+ } catch (e) {
5874
+ out({ type: "response", ok: false, text: "err: " + e.message });
5875
+ }
5876
+ });
5877
+ const shutdown = async () => {
5878
+ try {
5879
+ await rt.stop();
5880
+ } catch {
5881
+ }
5882
+ out({ type: "stopped", t: Date.now() });
5883
+ process.exit(0);
5884
+ };
5885
+ process.on("SIGINT", shutdown);
5886
+ process.on("SIGTERM", shutdown);
5887
+ await new Promise(() => {
5888
+ });
5889
+ }
5890
+ function profileSummary(cfg) {
5891
+ const stage = findStage(cfg.stage);
5892
+ return {
5893
+ slug: cfg.slug,
5894
+ name: cfg.name,
5895
+ age: cfg.age,
5896
+ mode: cfg.mode,
5897
+ nationality: cfg.nationality,
5898
+ tz: cfg.tz,
5899
+ stage: { id: cfg.stage, label: stage.label }
5900
+ };
5901
+ }
5902
+
5903
+ // src/server.ts
5904
+ init_esm_shims();
5905
+ import fs4 from "fs/promises";
5906
+ import path5 from "path";
5907
+ import os from "os";
5908
+ var SERVER_HELP = `
5909
+ girl-agent server \u2014 automation / ops mode (no TTY required)
5910
+
5911
+ usage:
5912
+ girl-agent server --print-config > bot.json
5913
+ # \u043E\u0442\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0439 bot.json
5914
+ girl-agent server --config bot.json --headless
5915
+
5916
+ girl-agent server --list
5917
+ girl-agent server --profile=<slug> --headless
5918
+
5919
+ girl-agent server --print-systemd > /etc/systemd/system/girl-agent.service
5920
+ girl-agent server --print-docker
5921
+
5922
+ env-vars (\u0434\u043B\u044F CI / docker secrets / k8s):
5923
+ GIRL_AGENT_DATA \u043F\u0443\u0442\u044C \u043A \u043F\u0440\u043E\u0444\u0438\u043B\u044F\u043C (default: ./data)
5924
+ GIRL_AGENT_MODE bot|userbot
5925
+ GIRL_AGENT_TOKEN telegram bot token
5926
+ GIRL_AGENT_API_PRESET openai|anthropic|claudehub|...
5927
+ GIRL_AGENT_API_KEY \u043A\u043B\u044E\u0447 \u043E\u0442 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430
5928
+ GIRL_AGENT_MODEL, _NAME, _AGE, _NATIONALITY, _TZ, _STAGE, _COMM_PRESET
5929
+
5930
+ \u0434\u043B\u044F \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u043F\u0435\u0440\u0432\u0438\u0447\u043D\u043E\u0439 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439 \u0431\u0435\u0437 \u0444\u043B\u0430\u0433\u043E\u0432 \u0432 \u043E\u0431\u044B\u0447\u043D\u043E\u043C \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435 \u2014
5931
+ \u043E\u0442\u043A\u0440\u043E\u0435\u0442\u0441\u044F ink-\u0432\u0438\u0437\u0430\u0440\u0434.
5932
+ `;
5933
+ function parseServerArgs(argv) {
5934
+ return {
5935
+ config: typeof argv.config === "string" ? argv.config : void 0,
5936
+ printConfig: !!argv["print-config"],
5937
+ printSystemd: !!argv["print-systemd"],
5938
+ printDocker: !!argv["print-docker"],
5939
+ headless: !!argv.headless,
5940
+ jsonEvents: !!argv["json-events"],
5941
+ noStart: !!argv["no-start"] || argv.start === false,
5942
+ profile: typeof argv.profile === "string" ? argv.profile : void 0,
5943
+ list: !!argv.list,
5944
+ help: !!argv.help
5945
+ };
5946
+ }
5947
+ async function runServer(rawArgv) {
5948
+ const args = parseServerArgs(rawArgv);
5949
+ if (args.help) {
5950
+ process.stdout.write(SERVER_HELP);
5951
+ return;
5952
+ }
5953
+ if (args.printConfig) {
5954
+ process.stdout.write(buildConfigTemplate());
5955
+ return;
5956
+ }
5957
+ if (args.printSystemd) {
5958
+ process.stdout.write(buildSystemdUnit());
5959
+ return;
5960
+ }
5961
+ if (args.printDocker) {
5962
+ process.stdout.write(buildDockerArtifacts());
5963
+ return;
5964
+ }
5965
+ if (args.list) {
5966
+ const list = await listProfiles();
5967
+ process.stdout.write(list.length ? list.join("\n") + "\n" : "(\u043D\u0435\u0442 \u043F\u0440\u043E\u0444\u0438\u043B\u0435\u0439)\n");
5968
+ process.stdout.write(`data: ${DATA_ROOT}
5969
+ `);
5970
+ return;
5971
+ }
5972
+ if (args.profile) {
5973
+ const cfg = await readConfig(args.profile);
5974
+ if (!cfg) {
5975
+ process.stderr.write(`profile not found: ${args.profile}
5976
+ `);
5977
+ process.stderr.write(`data dir: ${DATA_ROOT}
5978
+ `);
5979
+ process.exit(1);
5980
+ }
5981
+ await startRuntime(cfg, args);
5982
+ return;
5983
+ }
5984
+ if (args.config) {
5985
+ const cfg = await loadConfigFile(args.config);
5986
+ await persistAndMaybeStart(cfg, args);
5987
+ return;
5988
+ }
5989
+ const cfgFromEnv = configFromEnv();
5990
+ if (cfgFromEnv) {
5991
+ process.stderr.write("[server] \u043F\u0440\u043E\u0432\u0438\u0436\u0443 \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0438\u0437 env vars\n");
5992
+ await persistAndMaybeStart(cfgFromEnv, args);
5993
+ return;
5994
+ }
5995
+ process.stderr.write(SERVER_HELP);
5996
+ process.stderr.write("\n[server] \u0434\u043B\u044F \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u043E\u0439 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0437\u0430\u043F\u0443\u0441\u0442\u0438 \u0431\u0435\u0437 \u0444\u043B\u0430\u0433\u043E\u0432 \u0432 TTY-\u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B\u0435.\n");
5997
+ process.exit(1);
5998
+ }
5999
+ async function persistAndMaybeStart(cfg, args) {
6000
+ await writeConfig(cfg);
6001
+ process.stderr.write(`[server] \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D: ${path5.join(DATA_ROOT, cfg.slug)}
6002
+ `);
6003
+ if (cfg.llm.apiKey || findPreset(cfg.llm.presetId)?.apiKeyRequired === false) {
6004
+ try {
6005
+ process.stderr.write("[server] \u0433\u0435\u043D\u0435\u0440\u0438\u0440\u0443\u0435\u043C persona/speech/communication...\n");
6006
+ const llm = makeLLM(cfg.llm);
6007
+ const generated = await generatePersonaPack(llm, cfg.slug, cfg.name, cfg.age, cfg.nationality, cfg.personaNotes ?? "");
6008
+ cfg.busySchedule = generated.busySchedule;
6009
+ await writeConfig(cfg);
6010
+ process.stderr.write("[server] \u043F\u0435\u0440\u0441\u043E\u043D\u0430 \u0433\u043E\u0442\u043E\u0432\u0430.\n");
6011
+ } catch (e) {
6012
+ process.stderr.write(`[server] \u043E\u0448\u0438\u0431\u043A\u0430 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0438 \u043F\u0435\u0440\u0441\u043E\u043D\u044B: ${e?.message ?? e}
6013
+ `);
6014
+ process.stderr.write("[server] \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D, \u043D\u043E \u0431\u0435\u0437 persona.md. \u041C\u043E\u0436\u043D\u043E \u043F\u0435\u0440\u0435\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043F\u043E\u0437\u0436\u0435.\n");
6015
+ }
6016
+ } else {
6017
+ process.stderr.write("[server] api-\u043A\u043B\u044E\u0447 \u043D\u0435 \u0437\u0430\u0434\u0430\u043D \u2014 \u043F\u0440\u043E\u043F\u0443\u0441\u043A\u0430\u0435\u043C \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u044E \u043F\u0435\u0440\u0441\u043E\u043D\u044B.\n");
6018
+ }
6019
+ if (args.noStart) {
6020
+ process.stderr.write(`[server] --no-start: \u0437\u0430\u043F\u0443\u0441\u043A \u043F\u0440\u043E\u043F\u0443\u0449\u0435\u043D.
6021
+ `);
6022
+ return;
6023
+ }
6024
+ await startRuntime(cfg, args);
6025
+ }
6026
+ async function startRuntime(cfg, args) {
6027
+ const rt = new Runtime(cfg);
6028
+ await rt.start();
6029
+ const wantsHeadless = !!(args.headless || args.jsonEvents);
6030
+ if (wantsHeadless) {
6031
+ await runHeadlessJsonEvents(rt);
6032
+ return;
6033
+ }
6034
+ process.stderr.write(`[server] \u0431\u043E\u0442 \u0437\u0430\u043F\u0443\u0449\u0435\u043D: ${cfg.name} (${cfg.slug})
6035
+ `);
6036
+ rt.on("event", (e) => {
6037
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
6038
+ const t = e.type ?? "event";
6039
+ process.stdout.write(`${ts} ${t} ${JSON.stringify(e)}
6040
+ `);
6041
+ });
6042
+ const stop = async () => {
6043
+ process.stderr.write("[server] \u043E\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430...\n");
6044
+ await rt.stop();
6045
+ process.exit(0);
6046
+ };
6047
+ process.on("SIGINT", stop);
6048
+ process.on("SIGTERM", stop);
6049
+ }
6050
+ function configFromEnv() {
6051
+ const e = process.env;
6052
+ if (!e.GIRL_AGENT_MODE && !e.GIRL_AGENT_TOKEN && !e.GIRL_AGENT_API_KEY) return null;
6053
+ const mode = e.GIRL_AGENT_MODE === "userbot" ? "userbot" : "bot";
6054
+ const presetId = e.GIRL_AGENT_API_PRESET ?? "claudehub";
6055
+ const preset = findPreset(presetId);
6056
+ if (!preset) {
6057
+ process.stderr.write(`[server] unknown api preset in env: ${presetId}
6058
+ `);
6059
+ process.exit(1);
6060
+ }
6061
+ const nationality = e.GIRL_AGENT_NATIONALITY === "UA" ? "UA" : "RU";
6062
+ const name = e.GIRL_AGENT_NAME || pickRandomNames(nationality, 1)[0];
6063
+ const age = Number(e.GIRL_AGENT_AGE ?? 18);
6064
+ const tz = e.GIRL_AGENT_TZ ? parseTzFlag(e.GIRL_AGENT_TZ) ?? defaultTzForNationality(nationality) : defaultTzForNationality(nationality);
6065
+ const stage = e.GIRL_AGENT_STAGE || "tg-given-cold";
6066
+ const commPreset = COMMUNICATION_PRESETS.find((c) => c.id === (e.GIRL_AGENT_COMM_PRESET ?? "normal")) ?? COMMUNICATION_PRESETS[0];
6067
+ return {
6068
+ slug: slugify(name),
6069
+ name,
6070
+ age,
6071
+ nationality,
6072
+ tz,
6073
+ mode,
6074
+ stage,
6075
+ llm: {
6076
+ presetId,
6077
+ proto: preset.proto,
6078
+ baseURL: preset.baseURL,
6079
+ apiKey: e.GIRL_AGENT_API_KEY ?? preset.defaultApiKey ?? "",
6080
+ model: e.GIRL_AGENT_MODEL ?? preset.defaultModel
6081
+ },
6082
+ telegram: mode === "bot" ? { botToken: e.GIRL_AGENT_TOKEN ?? "" } : {
6083
+ apiId: Number(e.GIRL_AGENT_TG_API_ID ?? 0),
6084
+ apiHash: e.GIRL_AGENT_TG_API_HASH ?? "",
6085
+ phone: e.GIRL_AGENT_TG_PHONE ?? ""
6086
+ },
6087
+ mcp: [],
6088
+ privacy: "owner-only",
6089
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6090
+ sleepFrom: Number(e.GIRL_AGENT_SLEEP_FROM ?? 23),
6091
+ sleepTo: Number(e.GIRL_AGENT_SLEEP_TO ?? 8),
6092
+ nightWakeChance: Number(e.GIRL_AGENT_NIGHT_WAKE ?? 0.05),
6093
+ communication: commPreset.profile,
6094
+ vibe: commPreset.profile.messageStyle === "one-liners" ? "short" : "warm",
6095
+ busySchedule: []
6096
+ };
6097
+ }
6098
+ async function loadConfigFile(file) {
6099
+ const abs = path5.isAbsolute(file) ? file : path5.join(process.cwd(), file);
6100
+ let raw;
6101
+ try {
6102
+ raw = await fs4.readFile(abs, "utf-8");
6103
+ } catch (e) {
6104
+ process.stderr.write(`[server] \u043D\u0435 \u043C\u043E\u0433\u0443 \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C ${abs}: ${e?.message ?? e}
6105
+ `);
6106
+ process.exit(1);
6107
+ }
6108
+ let parsed;
6109
+ try {
6110
+ parsed = JSON.parse(raw);
6111
+ } catch (e) {
6112
+ process.stderr.write(`[server] ${abs} \u043D\u0435 \u044F\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u0432\u0430\u043B\u0438\u0434\u043D\u044B\u043C JSON: ${e?.message ?? e}
6113
+ `);
6114
+ process.exit(1);
6115
+ }
6116
+ return validateConfig(parsed);
6117
+ }
6118
+ function validateConfig(raw) {
6119
+ const c = raw;
6120
+ const errs = [];
6121
+ if (!c.name) errs.push("name");
6122
+ if (!c.age || c.age < 14 || c.age > 99) errs.push("age (14..99)");
6123
+ if (!c.nationality || c.nationality !== "RU" && c.nationality !== "UA") errs.push("nationality (RU|UA)");
6124
+ if (!c.tz) errs.push("tz");
6125
+ if (!c.mode || c.mode !== "bot" && c.mode !== "userbot") errs.push("mode (bot|userbot)");
6126
+ if (!c.stage) errs.push("stage");
6127
+ if (!c.llm?.presetId) errs.push("llm.presetId");
6128
+ if (!c.llm?.model) errs.push("llm.model");
6129
+ if (errs.length) {
6130
+ process.stderr.write(`[server] \u043A\u043E\u043D\u0444\u0438\u0433 \u043D\u0435\u0432\u0430\u043B\u0438\u0434\u0435\u043D, \u043D\u0435\u0434\u043E\u0441\u0442\u0430\u044E\u0449\u0438\u0435 \u043F\u043E\u043B\u044F:
6131
+ - ${errs.join("\n - ")}
6132
+ `);
6133
+ process.stderr.write(`[server] \u0441\u043C. \u0448\u0430\u0431\u043B\u043E\u043D: girl-agent server --print-config
6134
+ `);
6135
+ process.exit(1);
6136
+ }
6137
+ const filled = {
6138
+ slug: c.slug || slugify(c.name),
6139
+ name: c.name,
6140
+ age: c.age,
6141
+ nationality: c.nationality,
6142
+ tz: c.tz,
6143
+ mode: c.mode,
6144
+ stage: c.stage,
6145
+ llm: {
6146
+ presetId: c.llm.presetId,
6147
+ proto: c.llm.proto ?? findPreset(c.llm.presetId)?.proto ?? "openai",
6148
+ baseURL: c.llm.baseURL ?? findPreset(c.llm.presetId)?.baseURL,
6149
+ apiKey: c.llm.apiKey ?? "",
6150
+ model: c.llm.model
6151
+ },
6152
+ telegram: c.telegram ?? {},
6153
+ mcp: c.mcp ?? [],
6154
+ privacy: c.privacy ?? "owner-only",
6155
+ createdAt: c.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6156
+ sleepFrom: c.sleepFrom ?? 23,
6157
+ sleepTo: c.sleepTo ?? 8,
6158
+ nightWakeChance: c.nightWakeChance ?? 0.05,
6159
+ communication: c.communication ?? COMMUNICATION_PRESETS[0].profile,
6160
+ vibe: c.vibe,
6161
+ personaNotes: c.personaNotes,
6162
+ busySchedule: c.busySchedule ?? []
6163
+ };
6164
+ return filled;
6165
+ }
6166
+ function buildConfigTemplate() {
6167
+ const sample = {
6168
+ slug: "anya",
6169
+ name: "\u0410\u043D\u044F",
6170
+ age: 22,
6171
+ nationality: "RU",
6172
+ tz: "Europe/Moscow",
6173
+ mode: "bot",
6174
+ stage: "tg-given-cold",
6175
+ llm: {
6176
+ presetId: "claudehub",
6177
+ proto: "anthropic",
6178
+ baseURL: "https://api.claudehub.fun",
6179
+ apiKey: "REPLACE_ME",
6180
+ model: "claude-sonnet-4.6"
6181
+ },
6182
+ telegram: { botToken: "REPLACE_ME" },
6183
+ mcp: [],
6184
+ privacy: "owner-only",
6185
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
6186
+ sleepFrom: 23,
6187
+ sleepTo: 8,
6188
+ nightWakeChance: 0.05,
6189
+ communication: COMMUNICATION_PRESETS[0].profile,
6190
+ vibe: "warm",
6191
+ busySchedule: []
6192
+ };
6193
+ return JSON.stringify(sample, null, 2) + "\n";
6194
+ }
6195
+ function buildSystemdUnit() {
6196
+ const home = os.homedir();
6197
+ return `# /etc/systemd/system/girl-agent.service
6198
+ # install: sudo cp this.service /etc/systemd/system/girl-agent.service
6199
+ # sudo systemctl daemon-reload
6200
+ # sudo systemctl enable --now girl-agent
6201
+
6202
+ [Unit]
6203
+ Description=girl-agent (Telegram AI girl)
6204
+ After=network-online.target
6205
+ Wants=network-online.target
6206
+
6207
+ [Service]
6208
+ Type=simple
6209
+ User=%i
6210
+ WorkingDirectory=${home}
6211
+ ExecStart=${home}/.local/bin/girl-agent server --config ${home}/.config/girl-agent/bot.json --headless
6212
+ Restart=on-failure
6213
+ RestartSec=10
6214
+ StandardOutput=journal
6215
+ StandardError=journal
6216
+ Environment=NODE_ENV=production
6217
+ # uncomment for env-driven setup:
6218
+ # Environment=GIRL_AGENT_MODE=bot
6219
+ # Environment=GIRL_AGENT_TOKEN=...
6220
+ # Environment=GIRL_AGENT_API_PRESET=claudehub
6221
+ # Environment=GIRL_AGENT_API_KEY=...
6222
+
6223
+ [Install]
6224
+ WantedBy=multi-user.target
6225
+ `;
6226
+ }
6227
+ function buildDockerArtifacts() {
6228
+ return `# === \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 ===
6229
+ docker run -it --rm \\
6230
+ -v girl-agent-data:/data \\
6231
+ -e GIRL_AGENT_DATA=/data \\
6232
+ ghcr.io/thesashadev/girl-agent:latest
6233
+
6234
+ # === headless \u0441 \u0433\u043E\u0442\u043E\u0432\u044B\u043C \u043A\u043E\u043D\u0444\u0438\u0433\u043E\u043C ===
6235
+ docker run -d --name girl-agent --restart=unless-stopped \\
6236
+ -v girl-agent-data:/data \\
6237
+ -v "$PWD/bot.json:/config/bot.json:ro" \\
6238
+ -e GIRL_AGENT_DATA=/data \\
6239
+ ghcr.io/thesashadev/girl-agent:latest \\
6240
+ server --config /config/bot.json --headless
6241
+
6242
+ # === \u0442\u043E\u043B\u044C\u043A\u043E env vars (\u0431\u0435\u0437 \u0444\u0430\u0439\u043B\u0430) ===
6243
+ docker run -d --name girl-agent --restart=unless-stopped \\
6244
+ -v girl-agent-data:/data \\
6245
+ -e GIRL_AGENT_DATA=/data \\
6246
+ -e GIRL_AGENT_MODE=bot \\
6247
+ -e GIRL_AGENT_TOKEN=... \\
6248
+ -e GIRL_AGENT_API_PRESET=claudehub \\
6249
+ -e GIRL_AGENT_API_KEY=... \\
6250
+ -e GIRL_AGENT_NAME='\u0410\u043D\u044F' \\
6251
+ -e GIRL_AGENT_AGE=22 \\
6252
+ ghcr.io/thesashadev/girl-agent:latest \\
6253
+ server --headless
6254
+
6255
+ # === docker-compose.yml ===
6256
+ # version: "3.9"
6257
+ # services:
6258
+ # girl-agent:
6259
+ # image: ghcr.io/thesashadev/girl-agent:latest
6260
+ # command: ["server", "--config", "/config/bot.json", "--headless"]
6261
+ # environment:
6262
+ # GIRL_AGENT_DATA: /data
6263
+ # volumes:
6264
+ # - girl-agent-data:/data
6265
+ # - ./bot.json:/config/bot.json:ro
6266
+ # restart: unless-stopped
6267
+ # volumes:
6268
+ # girl-agent-data:
6269
+ `;
6270
+ }
6271
+
5579
6272
  // src/cli.tsx
5580
6273
  var HELP = `
5581
6274
  girl-agent \u2014 AI girl for Telegram
@@ -5587,7 +6280,15 @@ usage:
5587
6280
  npx girl-agent --reset --profile=<slug>
5588
6281
  npx girl-agent <flags> # \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0432\u0438\u0437\u0430\u0440\u0434 \u0441 \u0430\u0440\u0433\u0443\u043C\u0435\u043D\u0442\u0430\u043C\u0438
5589
6282
 
5590
- required flags \u0434\u043B\u044F headless setup (--name --age --stage --api-preset --api-key --mode):
6283
+ server (\u0434\u043B\u044F \u0441\u0438\u0441\u0442\u0435\u043C \u0431\u0435\u0437 TTY: docker / systemd / cron / CI):
6284
+ npx girl-agent server --print-config > bot.json
6285
+ npx girl-agent server --config bot.json --headless
6286
+ npx girl-agent server --print-systemd | --print-docker | --list
6287
+
6288
+ \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043A\u0430 \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0431\u0435\u0437 node \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435):
6289
+ curl -fsSL https://raw.githubusercontent.com/TheSashaDev/girl-agent/main/scripts/install.sh | sh
6290
+
6291
+ required flags \u0434\u043B\u044F headless setup (--name --age --stage --api-preset --mode; --api-key \u043D\u0443\u0436\u0435\u043D \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u043E\u0432 \u0441 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0435\u0439):
5591
6292
  --profile=<slug> slug \u043F\u0440\u043E\u0444\u0438\u043B\u044F
5592
6293
  --mode=bot|userbot
5593
6294
  --token=<bot_token> \u0434\u043B\u044F bot
@@ -5596,7 +6297,7 @@ required flags \u0434\u043B\u044F headless setup (--name --age --stage --api-pre
5596
6297
  --base-url=<url> \u0434\u043B\u044F custom
5597
6298
  --proto=openai|anthropic \u0434\u043B\u044F custom
5598
6299
  --model=<model>
5599
- --api-key=<key>
6300
+ --api-key=<key> \u043D\u0435 \u043D\u0443\u0436\u0435\u043D \u0434\u043B\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0445 LM Studio/Ollama
5600
6301
  --name=<\u0438\u043C\u044F> \u043A\u043E\u043D\u043A\u0440\u0435\u0442\u043D\u043E\u0435 \u0438\u043C\u044F; \u0435\u0441\u043B\u0438 \u043F\u0440\u043E\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u2014 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u043E\u0435 \u0438\u0437 \u043F\u0443\u043B\u0430 \u043F\u043E nationality (\u0442\u0443\u0440\u043D\u0438\u0440 \u0432\u044B\u0431\u043E\u0440\u0430 \u0438\u043C\u0451\u043D \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u0422\u041E\u041B\u042C\u041A\u041E \u0432 TUI \u0432\u0438\u0437\u0430\u0440\u0434\u0435)
5601
6302
  --age=<n>
5602
6303
  --persona-notes=<text> \u0434\u043E\u043F. \u043F\u043E\u0436\u0435\u043B\u0430\u043D\u0438\u044F \u043A persona/speech/communication \u043F\u0435\u0440\u0435\u0434 \u0433\u0435\u043D\u0435\u0440\u0430\u0446\u0438\u0435\u0439
@@ -5642,15 +6343,71 @@ async function main() {
5642
6343
  "message-style",
5643
6344
  "initiative",
5644
6345
  "life-sharing",
5645
- "privacy"
6346
+ "privacy",
6347
+ "config"
6348
+ ],
6349
+ boolean: [
6350
+ "help",
6351
+ "list",
6352
+ "reset",
6353
+ "new",
6354
+ "json-events",
6355
+ "headless",
6356
+ "server",
6357
+ "print-config",
6358
+ "print-systemd",
6359
+ "print-docker",
6360
+ "no-start"
5646
6361
  ],
5647
- boolean: ["help", "list", "reset", "new"],
5648
6362
  alias: { h: "help" }
5649
6363
  });
6364
+ const positional = argv._ ?? [];
6365
+ const isServer = positional[0] === "server" || !!argv.server || !!argv["print-config"] || !!argv["print-systemd"] || !!argv["print-docker"];
6366
+ if (isServer) {
6367
+ await runServer(argv);
6368
+ return;
6369
+ }
5650
6370
  if (argv.help) {
5651
6371
  process.stdout.write(HELP);
5652
6372
  return;
5653
6373
  }
6374
+ const isHeadless = !!(argv["json-events"] || argv.headless || argv.list || argv.help);
6375
+ if (!isHeadless) {
6376
+ const stdin = process.stdin;
6377
+ const stdout = process.stdout;
6378
+ const stdinOk = !!stdin.isTTY;
6379
+ const stdoutOk = !!stdout.isTTY;
6380
+ if (!stdinOk || !stdoutOk) {
6381
+ process.stderr.write(
6382
+ `
6383
+ [girl-agent] \u044D\u0442\u043E\u0442 \u0442\u0435\u0440\u043C\u0438\u043D\u0430\u043B \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0438\u043D\u0442\u0435\u0440\u0430\u043A\u0442\u0438\u0432\u043D\u044B\u0439 ink-\u0432\u0438\u0437\u0430\u0440\u0434 (\u043D\u0435\u0442 TTY).
6384
+ stdin.isTTY = ${stdinOk}, stdout.isTTY = ${stdoutOk}
6385
+
6386
+ \u0447\u0442\u043E \u0434\u0435\u043B\u0430\u0442\u044C (\u0434\u043B\u044F \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u0432 / docker / ssh \u0431\u0435\u0437 -t / cron / CI):
6387
+
6388
+ 1. \u043F\u043E\u0441\u0442\u0430\u0432\u044C \u0441\u0435\u0431\u0435 girl-agent \u043E\u0434\u043D\u043E\u0439 \u043A\u043E\u043C\u0430\u043D\u0434\u043E\u0439 (\u0431\u0435\u0437 node \u043D\u0430 \u043C\u0430\u0448\u0438\u043D\u0435):
6389
+ curl -fsSL https://raw.githubusercontent.com/TheSashaDev/girl-agent/main/scripts/install.sh | sh
6390
+ \u0434\u0430\u043B\u044C\u0448\u0435: girl-agent # ink-\u0432\u0438\u0437\u0430\u0440\u0434 \u0432 \u043E\u0431\u044B\u0447\u043D\u043E\u043C tty
6391
+
6392
+ 2. \u0433\u043E\u0442\u043E\u0432\u044B\u0439 \u043A\u043E\u043D\u0444\u0438\u0433 + headless (\u0434\u043B\u044F systemd / cron / CI):
6393
+ girl-agent server --print-config > bot.json
6394
+ # \u043E\u0442\u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0443\u0439 bot.json
6395
+ girl-agent server --config bot.json --headless
6396
+
6397
+ 3. docker (\u0432\u0441\u0451 \u0432\u043D\u0443\u0442\u0440\u0438 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430, \u043D\u043E\u043B\u044C \u0437\u0430\u0432\u0438\u0441\u0438\u043C\u043E\u0441\u0442\u0435\u0439 \u043D\u0430 \u0445\u043E\u0441\u0442\u0435):
6398
+ docker run -it --rm -v girl-agent-data:/data \\
6399
+ ghcr.io/thesashadev/girl-agent:latest
6400
+
6401
+ 4. systemd: girl-agent server --print-systemd
6402
+ docker: girl-agent server --print-docker
6403
+
6404
+ 5. \u043D\u0430 windows \u0431\u044B\u0441\u0442\u0440\u0435\u0435 \u0432\u0441\u0435\u0433\u043E \u2014 \u0433\u0440\u0430\u0444\u0438\u0447\u0435\u0441\u043A\u0438\u0439 \u0438\u043D\u0441\u0442\u0430\u043B\u043B\u0435\u0440 girl-agent-installer.exe.
6405
+ `
6406
+ );
6407
+ process.exit(2);
6408
+ }
6409
+ }
6410
+ const jsonEvents = !!(argv["json-events"] || argv.headless);
5654
6411
  if (argv.age != null) {
5655
6412
  const a = Number(argv.age);
5656
6413
  if (!Number.isFinite(a) || a < 13 || a > 99) {
@@ -5680,10 +6437,12 @@ ${profiles.join("\n")}
5680
6437
  cfg.stage = "tg-given-cold";
5681
6438
  await writeConfig(cfg);
5682
6439
  }
5683
- await runRuntime(cfg);
6440
+ await runRuntime(cfg, { jsonEvents });
5684
6441
  return;
5685
6442
  }
5686
- const haveEnoughForFlags = argv.mode && argv["api-preset"] && argv["api-key"] && argv.age && argv.stage;
6443
+ const presetForFlags = argv["api-preset"] ? findPreset(String(argv["api-preset"])) : void 0;
6444
+ const apiKeyRequiredForFlags = presetForFlags?.apiKeyRequired !== false;
6445
+ const haveEnoughForFlags = argv.mode && argv["api-preset"] && (!apiKeyRequiredForFlags || argv["api-key"]) && argv.age && argv.stage;
5687
6446
  if (haveEnoughForFlags) {
5688
6447
  const cfg = await buildConfigFromFlags(argv);
5689
6448
  await writeConfig(cfg);
@@ -5694,7 +6453,7 @@ ${profiles.join("\n")}
5694
6453
  const generated = await generatePersonaPack(llm, cfg.slug, cfg.name, cfg.age, cfg.nationality, personaNotesForGeneration2(cfg));
5695
6454
  cfg.busySchedule = generated.busySchedule;
5696
6455
  await writeConfig(cfg);
5697
- await runRuntime(cfg);
6456
+ await runRuntime(cfg, { jsonEvents });
5698
6457
  return;
5699
6458
  }
5700
6459
  if (!argv.new && !argv.profile && !haveEnoughForFlags) {
@@ -5704,7 +6463,7 @@ ${profiles.join("\n")}
5704
6463
  if (cfg) {
5705
6464
  process.stdout.write(`\u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044E \u043F\u0440\u043E\u0444\u0438\u043B\u044C: ${cfg.name}
5706
6465
  `);
5707
- await runRuntime(cfg);
6466
+ await runRuntime(cfg, { jsonEvents });
5708
6467
  return;
5709
6468
  }
5710
6469
  } else if (profiles.length > 1) {
@@ -5720,7 +6479,7 @@ ${profiles.join("\n")}
5720
6479
  const inst = render(
5721
6480
  /* @__PURE__ */ React3.createElement(Wizard, { onDone: async (cfg) => {
5722
6481
  inst.unmount();
5723
- await runRuntime(cfg);
6482
+ await runRuntime(cfg, { jsonEvents });
5724
6483
  resolve();
5725
6484
  } }),
5726
6485
  { exitOnCtrlC: true }
@@ -5755,7 +6514,7 @@ async function buildConfigFromFlags(argv) {
5755
6514
  tz,
5756
6515
  mode,
5757
6516
  stage: argv.stage,
5758
- llm: { presetId, proto, baseURL, apiKey: String(argv["api-key"]), model },
6517
+ llm: { presetId, proto, baseURL, apiKey: String(argv["api-key"] ?? preset?.defaultApiKey ?? ""), model },
5759
6518
  telegram: mode === "bot" ? { botToken: String(argv.token ?? "") } : {
5760
6519
  apiId: Number(argv["api-id"] ?? 0),
5761
6520
  apiHash: String(argv["api-hash"] ?? ""),
@@ -5793,9 +6552,13 @@ function personaNotesForGeneration2(cfg) {
5793
6552
  ].filter(Boolean);
5794
6553
  return parts.join("\n\n");
5795
6554
  }
5796
- async function runRuntime(cfg) {
6555
+ async function runRuntime(cfg, opts = {}) {
5797
6556
  const rt = new Runtime(cfg);
5798
6557
  await rt.start();
6558
+ if (opts.jsonEvents) {
6559
+ await runHeadlessJsonEvents(rt);
6560
+ return;
6561
+ }
5799
6562
  const inst = render(/* @__PURE__ */ React3.createElement(Dashboard, { runtime: rt }), { exitOnCtrlC: true });
5800
6563
  process.on("SIGINT", async () => {
5801
6564
  await rt.stop();
@@ -5805,7 +6568,17 @@ async function runRuntime(cfg) {
5805
6568
  await inst.waitUntilExit();
5806
6569
  await rt.stop();
5807
6570
  }
6571
+ process.on("unhandledRejection", (reason) => {
6572
+ const r = reason;
6573
+ const text = typeof r === "object" && r && r.stack ? r.stack : String(reason);
6574
+ process.stderr.write("[girl-agent] unhandled rejection: " + text + "\n");
6575
+ process.exit(1);
6576
+ });
6577
+ process.on("uncaughtException", (err) => {
6578
+ process.stderr.write("[girl-agent] uncaught: " + (err?.stack ?? err) + "\n");
6579
+ process.exit(1);
6580
+ });
5808
6581
  main().catch((e) => {
5809
- process.stderr.write("fatal: " + (e?.stack ?? e) + "\n");
6582
+ process.stderr.write("[girl-agent] fatal: " + (e?.stack ?? e) + "\n");
5810
6583
  process.exit(1);
5811
6584
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thesashadev/girl-agent",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Telegram AI persona engine with memory, schedule, relationship state and MTProto userbot mode.",
5
5
  "type": "module",
6
6
  "bin": {