@thesashadev/girl-agent 0.1.8 → 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,35 @@
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
+
3
33
  ## 0.1.8 — OpenAI-compatible API compatibility
4
34
 
5
35
  Дата: 2026-05-06
@@ -7,6 +37,7 @@
7
37
  - JSON-ответы теперь сначала запрашиваются через `json_schema`, с fallback на `json_object` и `text` для разных OpenAI-compatible API. (#33)
8
38
  - LM Studio и Ollama больше не требуют реальный API ключ в wizard/headless setup.
9
39
  - Добавлена совместимость с OpenAI-compatible прокси, которые возвращают SSE/event-stream даже на обычный chat completions запрос.
40
+ - Добавлена Docker-поддержка для 24/7 запуска на сервере: `Dockerfile`, `docker-compose.yml`, volume для `data` и инструкции в README. (#35)
10
41
 
11
42
  ## 0.1.7 — MarkdownV2 escaping fix
12
43
 
package/README.md CHANGED
@@ -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,35 +37,97 @@
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`
45
73
 
46
- Если профиль уже есть:
74
+ ### windows десктоп-приложение
75
+
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
 
52
- **Через Docker (рекомендуется для сервера):**
93
+ ### docker (для серверов; нулевые зависимости на хосте)
94
+
95
+ Интерактивная первичная настройка (ink-визард внутри контейнера):
96
+ ```sh
97
+ docker run -it --rm -v girl-agent-data:/data ghcr.io/thesashadev/girl-agent:latest
98
+ ```
53
99
 
54
- Первый запуск с интерактивом (для настройки через визард):
55
- ```bash
56
- docker-compose run --rm -it girl-agent
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
57
111
  ```
58
- *(пройдите все шаги и после появления дашборда нажмите `Ctrl+C`)*
59
112
 
60
- Последующие (запуск в фоне):
61
- ```bash
62
- docker-compose up -d
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
63
124
  ```
64
- *(если профилей несколько, запустите конкретный так: `docker-compose run -d girl-agent node dist/cli.js --profile=arina`)*
65
125
 
66
- *(посмотреть логи: `docker-compose logs -f`)*
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) в корне репо
67
131
 
68
132
  **Из исходников:**
69
133
 
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) {
@@ -800,7 +809,7 @@ function sameProfile(a, b) {
800
809
  init_esm_shims();
801
810
  import { promises as fs } from "fs";
802
811
  import path2 from "path";
803
- 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");
804
813
  function profileDir(slug) {
805
814
  return path2.join(DATA_ROOT, slug);
806
815
  }
@@ -1534,14 +1543,14 @@ var DEFAULT_PROXY = "https://tgproxy.girl-agent.com";
1534
1543
  function proxyUrl() {
1535
1544
  return process.env.GIRL_AGENT_AUTH_PROXY ?? DEFAULT_PROXY;
1536
1545
  }
1537
- async function post(path5, body) {
1538
- const res = await fetch(`${proxyUrl()}${path5}`, {
1546
+ async function post(path6, body) {
1547
+ const res = await fetch(`${proxyUrl()}${path6}`, {
1539
1548
  method: "POST",
1540
1549
  headers: { "Content-Type": "application/json" },
1541
1550
  body: JSON.stringify(body)
1542
1551
  });
1543
1552
  const data = await res.json();
1544
- 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})`);
1545
1554
  return data;
1546
1555
  }
1547
1556
  function remoteSendCode(phone) {
@@ -3093,8 +3102,8 @@ phoneAvailable=false \u043A\u043E\u0433\u0434\u0430: \u0441\u043F\u0438\u0442, \
3093
3102
  }
3094
3103
  async function loadOrGenerateDailyLife(llm, cfg, now = /* @__PURE__ */ new Date(), conflict = null) {
3095
3104
  const dateLocal = localDateStr(cfg.tz, now);
3096
- const path5 = `daily-life/${dateLocal}.md`;
3097
- const existing = await readMd(cfg.slug, path5);
3105
+ const path6 = `daily-life/${dateLocal}.md`;
3106
+ const existing = await readMd(cfg.slug, path6);
3098
3107
  if (existing) {
3099
3108
  try {
3100
3109
  const m = existing.match(/<!--daily:(.+?)-->/s);
@@ -3126,7 +3135,7 @@ async function loadOrGenerateDailyLife(llm, cfg, now = /* @__PURE__ */ new Date(
3126
3135
  dl = { dateLocal, vibe: "\u043E\u0431\u044B\u0447\u043D\u044B\u0439 \u0434\u0435\u043D\u044C", blocks: [], events: [], wants: [] };
3127
3136
  }
3128
3137
  const human = renderDailyLifeHuman(dl);
3129
- await writeMd(cfg.slug, path5, `${human}
3138
+ await writeMd(cfg.slug, path6, `${human}
3130
3139
 
3131
3140
  <!--daily:${JSON.stringify(dl)}-->
3132
3141
  `);
@@ -3356,9 +3365,9 @@ async function ensureDefaults(cfg) {
3356
3365
  ["time/promises.md", "# promises\n"],
3357
3366
  ["memory/uncertain.md", "# uncertain\n"]
3358
3367
  ];
3359
- await Promise.all(defaults.map(async ([path5, content]) => {
3360
- const current = await readMd(cfg.slug, path5);
3361
- 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");
3362
3371
  }));
3363
3372
  }
3364
3373
  async function loadRealismContext(cfg, incoming) {
@@ -5730,6 +5739,536 @@ function sleep(ms) {
5730
5739
  return new Promise((r) => setTimeout(r, ms));
5731
5740
  }
5732
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
+
5733
6272
  // src/cli.tsx
5734
6273
  var HELP = `
5735
6274
  girl-agent \u2014 AI girl for Telegram
@@ -5741,6 +6280,14 @@ usage:
5741
6280
  npx girl-agent --reset --profile=<slug>
5742
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
5743
6282
 
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
+
5744
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):
5745
6292
  --profile=<slug> slug \u043F\u0440\u043E\u0444\u0438\u043B\u044F
5746
6293
  --mode=bot|userbot
@@ -5796,15 +6343,71 @@ async function main() {
5796
6343
  "message-style",
5797
6344
  "initiative",
5798
6345
  "life-sharing",
5799
- "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"
5800
6361
  ],
5801
- boolean: ["help", "list", "reset", "new"],
5802
6362
  alias: { h: "help" }
5803
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
+ }
5804
6370
  if (argv.help) {
5805
6371
  process.stdout.write(HELP);
5806
6372
  return;
5807
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);
5808
6411
  if (argv.age != null) {
5809
6412
  const a = Number(argv.age);
5810
6413
  if (!Number.isFinite(a) || a < 13 || a > 99) {
@@ -5834,7 +6437,7 @@ ${profiles.join("\n")}
5834
6437
  cfg.stage = "tg-given-cold";
5835
6438
  await writeConfig(cfg);
5836
6439
  }
5837
- await runRuntime(cfg);
6440
+ await runRuntime(cfg, { jsonEvents });
5838
6441
  return;
5839
6442
  }
5840
6443
  const presetForFlags = argv["api-preset"] ? findPreset(String(argv["api-preset"])) : void 0;
@@ -5850,7 +6453,7 @@ ${profiles.join("\n")}
5850
6453
  const generated = await generatePersonaPack(llm, cfg.slug, cfg.name, cfg.age, cfg.nationality, personaNotesForGeneration2(cfg));
5851
6454
  cfg.busySchedule = generated.busySchedule;
5852
6455
  await writeConfig(cfg);
5853
- await runRuntime(cfg);
6456
+ await runRuntime(cfg, { jsonEvents });
5854
6457
  return;
5855
6458
  }
5856
6459
  if (!argv.new && !argv.profile && !haveEnoughForFlags) {
@@ -5860,7 +6463,7 @@ ${profiles.join("\n")}
5860
6463
  if (cfg) {
5861
6464
  process.stdout.write(`\u0437\u0430\u0433\u0440\u0443\u0436\u0430\u044E \u043F\u0440\u043E\u0444\u0438\u043B\u044C: ${cfg.name}
5862
6465
  `);
5863
- await runRuntime(cfg);
6466
+ await runRuntime(cfg, { jsonEvents });
5864
6467
  return;
5865
6468
  }
5866
6469
  } else if (profiles.length > 1) {
@@ -5876,7 +6479,7 @@ ${profiles.join("\n")}
5876
6479
  const inst = render(
5877
6480
  /* @__PURE__ */ React3.createElement(Wizard, { onDone: async (cfg) => {
5878
6481
  inst.unmount();
5879
- await runRuntime(cfg);
6482
+ await runRuntime(cfg, { jsonEvents });
5880
6483
  resolve();
5881
6484
  } }),
5882
6485
  { exitOnCtrlC: true }
@@ -5949,9 +6552,13 @@ function personaNotesForGeneration2(cfg) {
5949
6552
  ].filter(Boolean);
5950
6553
  return parts.join("\n\n");
5951
6554
  }
5952
- async function runRuntime(cfg) {
6555
+ async function runRuntime(cfg, opts = {}) {
5953
6556
  const rt = new Runtime(cfg);
5954
6557
  await rt.start();
6558
+ if (opts.jsonEvents) {
6559
+ await runHeadlessJsonEvents(rt);
6560
+ return;
6561
+ }
5955
6562
  const inst = render(/* @__PURE__ */ React3.createElement(Dashboard, { runtime: rt }), { exitOnCtrlC: true });
5956
6563
  process.on("SIGINT", async () => {
5957
6564
  await rt.stop();
@@ -5961,7 +6568,17 @@ async function runRuntime(cfg) {
5961
6568
  await inst.waitUntilExit();
5962
6569
  await rt.stop();
5963
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
+ });
5964
6581
  main().catch((e) => {
5965
- process.stderr.write("fatal: " + (e?.stack ?? e) + "\n");
6582
+ process.stderr.write("[girl-agent] fatal: " + (e?.stack ?? e) + "\n");
5966
6583
  process.exit(1);
5967
6584
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thesashadev/girl-agent",
3
- "version": "0.1.8",
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": {