zames_pro 2.73.3 → 2.73.4

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
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.73.4] - 2026-10-11
11
+
12
+ ### Changed
13
+
14
+ - docs(readme): restore mermaid-style diagram with Qwen, finish provider-neutral prose
15
+
10
16
  ## [2.73.3] - 2026-10-11
11
17
 
12
18
  ### Fixed
package/README.md CHANGED
@@ -28,8 +28,8 @@ through Playwright.
28
28
  In spirit it is similar to Claude Code / Codex CLI: it starts in the current
29
29
  directory, reads and edits files, runs commands, and commits to git.
30
30
 
31
- > No API key required — it drives the DeepSeek web chat like a regular user
32
- > through a real (headless) browser.
31
+ > No API key required — it drives a DeepSeek or Qwen web chat like a regular
32
+ > user through a real (headless) browser.
33
33
 
34
34
  <p align="center">
35
35
  <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames session in the terminal" width="860">
@@ -44,10 +44,12 @@ zames # sign in once, then describe your task
44
44
  ```
45
45
 
46
46
  There is **no API key and no per-token bill**: zames signs in to **your own
47
- [chat.deepseek.com](https://chat.deepseek.com/) account** in a real (headless)
48
- Chromium and drives the web chat like a regular user. The first launch asks for
49
- your DeepSeek login once (the session is stored in `~/.zames/profile`); after
50
- that just run `zames` and type a task in plain language:
47
+ [chat.deepseek.com](https://chat.deepseek.com/) or
48
+ [chat.qwen.ai](https://chat.qwen.ai/) account** in a real (headless) Chromium
49
+ and drives the web chat like a regular user. The first launch asks for your
50
+ login once (the session is stored in `~/.zames/profile` for DeepSeek or
51
+ `~/.zames/qwen-profile` for Qwen); after that just run `zames` and type a task
52
+ in plain language:
51
53
 
52
54
  ```text
53
55
  ❯ refactor the config loader and add a test for the new default
@@ -122,8 +124,8 @@ Key pieces:
122
124
 
123
125
  ## Why zames?
124
126
 
125
- - **No API key, no per-token bill** — it uses your own DeepSeek chat account,
126
- not the paid API. Good for long, tool-heavy tasks.
127
+ - **No API key, no per-token bill** — it uses your own DeepSeek or Qwen chat
128
+ account, not the paid API. Good for long, tool-heavy tasks.
127
129
  - **Same workflow as Claude Code / Codex** — tools, `AGENTS.md`, skills, MCP
128
130
  and slash commands, so it feels familiar from day one.
129
131
  - **Runs unattended** — headless by default, resumable sessions, and `/loop`
@@ -143,11 +145,11 @@ Key pieces:
143
145
 
144
146
  ## Signing in
145
147
 
146
- The browser runs **headless by default**. When DeepSeek requires a sign-in,
147
- zames:
148
+ The browser runs **headless by default**. When the active backend requires a
149
+ sign-in, zames:
148
150
 
149
- 1. reuses the session stored in the persistent profile (`~/.zames/profile`)
150
- if it is still valid;
151
+ 1. reuses the session stored in the persistent profile (`~/.zames/profile` for
152
+ DeepSeek, `~/.zames/qwen-profile` for Qwen) if it is still valid;
151
153
  2. otherwise signs in automatically with the saved credentials
152
154
  (`browser.auth.username` / `browser.auth.password`);
153
155
  3. otherwise asks you for the login/password in the terminal (in a TTY) and,
@@ -165,7 +167,7 @@ The `--headless` flag (the default) and `headless: true` in the config keep
165
167
  the browser without a window; `--headed` / `headless: false` show it.
166
168
 
167
169
  Headless works out of the box: a headless Chrome normally advertises a
168
- `HeadlessChrome/...` User-Agent that DeepSeek's CDN blocks with a 403, so
170
+ `HeadlessChrome/...` User-Agent that the site's CDN blocks with a 403, so
169
171
  zames strips that marker before loading the page (keeping the real engine
170
172
  version). You do not need `--headed` just to log in.
171
173
 
@@ -233,7 +235,7 @@ The agent works inside the directory it was started in and cannot leave it (sand
233
235
  While the agent is working you can keep typing: press Enter to queue a message
234
236
  (it is sent right after the current task, in the same chat), or Esc / Ctrl+C to
235
237
  abort the current generation. This mirrors typing during generation on the
236
- DeepSeek website.
238
+ web chat.
237
239
 
238
240
  ### Input line
239
241
 
@@ -261,8 +263,8 @@ You can paste an image or a file into the input line (Ctrl+Shift+V / Shift+Inser
261
263
  or the terminal's own paste). zames saves it under `<project>/tmp` and shows a
262
264
  marker in the line - `[image#1]` for images, `[file#1]` for other files - then
263
265
  uploads the real file to the chat together with your message. These are the
264
- same formats the DeepSeek web chat accepts (PNG, JPEG, GIF, WEBP, BMP, SVG and
265
- the usual document types).
266
+ same formats the DeepSeek / Qwen web chat accepts (PNG, JPEG, GIF, WEBP, BMP,
267
+ SVG and the usual document types).
266
268
 
267
269
  Pasting an image works when the terminal sends it as a `data:` URL or as a
268
270
  base64 blob with a recognizable image signature. You can also paste a path to a
@@ -369,7 +371,7 @@ Codex CLI:
369
371
  (Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush) are removed
370
372
  from the tool set, so the agent can investigate without touching the tree.
371
373
  Start in it with `--plan`.
372
- - /compact — ask DeepSeek to compress the current chat into a handover
374
+ - /compact — ask the active backend to compress the current chat into a handover
373
375
  summary, then open a NEW chat, resend the system prompt and post the summary
374
376
  as the carried-over context. Use it when the context gets long.
375
377
  - /goal [text|clear] — set a long-lived session goal. It is prepended to every
@@ -505,31 +507,34 @@ Agent data is stored in `~/.zames`: browser profile, logs, undo history, session
505
507
 
506
508
  ## FAQ
507
509
 
508
- **Is this an official DeepSeek product?**
509
- No. zames drives the public chat.deepseek.com web UI through a real browser,
510
- like a regular user. It is not affiliated with DeepSeek.
510
+ **Is this an official DeepSeek or Qwen product?**
511
+ No. zames drives the public `chat.deepseek.com` / `chat.qwen.ai` web UIs through
512
+ a real browser, like a regular user. It is not affiliated with DeepSeek or
513
+ Alibaba.
511
514
 
512
515
  **Do I need an API key?**
513
- No. You sign in with your own DeepSeek account once; the session is stored in
514
- `~/.zames/profile` and reused.
516
+ No. You sign in with your own account on the chat you use — DeepSeek (stored in
517
+ `~/.zames/profile`) or Qwen (stored in `~/.zames/qwen-profile`) — and the session
518
+ is reused. Qwen also works in guest mode for a few messages a day.
515
519
 
516
520
  **Which model does it use?**
517
- Whichever the DeepSeek web chat uses (DeepSeek-V3, or the reasoning model with
518
- "Deep thinking" on). zames never calls the API directly.
521
+ Whichever the active web chat uses. On DeepSeek that is DeepSeek-V3 (or the
522
+ reasoning model with "Deep thinking" on). On Qwen you can pick a model with
523
+ `/model` (e.g. `qwen3.7-plus`). zames never calls the API directly.
519
524
 
520
525
  **Does it work headless / on a server?**
521
526
  Yes — headless is the default, and `--output-format jsonl` makes it scriptable.
522
527
  `--headed` is only needed for a manual sign-in or selector debugging.
523
528
 
524
529
  **Is automating the web UI allowed?**
525
- That depends on DeepSeek's terms; automating a website may violate them. Use at
526
- your own risk and keep the send throttle (15s by default) so you do not hammer
527
- the rate limit.
530
+ That depends on the site's terms; automating a website may violate them. Use at
531
+ your own risk and keep the send throttle (15 s on DeepSeek by default) so you do
532
+ not hammer the rate limit.
528
533
 
529
534
  **How is it different from Claude Code / Codex?**
530
535
  Same shape (tools, `AGENTS.md`, skills, MCP, slash commands) but it runs on your
531
- DeepSeek account instead of an API, as a browser automation rather than a
532
- first-party API client.
536
+ DeepSeek or Qwen account instead of an API, as a browser automation rather than
537
+ a first-party API client.
533
538
 
534
539
  ## Links
535
540
 
@@ -554,7 +559,7 @@ MIT
554
559
  По духу он похож на Claude Code / Codex CLI: запускается в текущей
555
560
  директории, читает и правит файлы, выполняет команды и коммитит в git.
556
561
 
557
- > API-ключ не нужен — агент управляет веб-чатом DeepSeek как обычный
562
+ > API-ключ не нужен — агент управляет веб-чатом DeepSeek или Qwen как обычный
558
563
  > пользователь через настоящий (headless) браузер.
559
564
 
560
565
  <p align="center">
@@ -570,11 +575,12 @@ zames # один раз войти, дальше прост
570
575
  ```
571
576
 
572
577
  Здесь **нет API-ключа и нет оплаты за токены**: zames входит в **ваш
573
- собственный аккаунт [chat.deepseek.com](https://chat.deepseek.com/)** в
574
- настоящем (headless) Chromium и управляет веб-чатом как обычный пользователь.
575
- При первом запуске он один раз спросит логин DeepSeek (сессия сохраняется в
576
- `~/.zames/profile`); после этого достаточно запустить `zames` и написать
577
- задачу обычным языком:
578
+ собственный аккаунт [chat.deepseek.com](https://chat.deepseek.com/) или
579
+ [chat.qwen.ai](https://chat.qwen.ai/)** в настоящем (headless) Chromium и
580
+ управляет веб-чатом как обычный пользователь. При первом запуске он один раз
581
+ спросит логин (сессия сохраняется в `~/.zames/profile` для DeepSeek или
582
+ `~/.zames/qwen-profile` для Qwen); после этого достаточно запустить `zames` и
583
+ написать задачу обычным языком:
578
584
 
579
585
  ```text
580
586
  ❯ отрефактори загрузчик конфига и добавь тест на новый дефолт
@@ -654,7 +660,8 @@ zames не вызывает API модели. Он запускает headless C
654
660
  ## Почему zames?
655
661
 
656
662
  - **Нет API-ключа и счёта за токены** — используется ваш собственный аккаунт
657
- чата DeepSeek, а не платный API. Удобно для долгих задач с инструментами.
663
+ чата DeepSeek или Qwen, а не платный API. Удобно для долгих задач с
664
+ инструментами.
658
665
  - **Тот же рабочий процесс, что в Claude Code / Codex** — инструменты,
659
666
  `AGENTS.md`, навыки, MCP и слэш-команды, так что всё знакомо с первого дня.
660
667
  - **Работает без присмотра** — headless по умолчанию, возобновляемые сессии,
@@ -674,11 +681,11 @@ zames не вызывает API модели. Он запускает headless C
674
681
 
675
682
  ## Вход в аккаунт
676
683
 
677
- Браузер по умолчанию работает **headless**. Когда DeepSeek требует входа,
678
- zames:
684
+ Браузер по умолчанию работает **headless**. Когда активный бэкенд требует
685
+ входа, zames:
679
686
 
680
- 1. переиспользует сессию из постоянного профиля (`~/.zames/profile`), если она
681
- ещё валидна;
687
+ 1. переиспользует сессию из постоянного профиля (`~/.zames/profile` для
688
+ DeepSeek, `~/.zames/qwen-profile` для Qwen), если она ещё валидна;
682
689
  2. иначе входит автоматически по сохранённым учётным данным
683
690
  (`browser.auth.username` / `browser.auth.password`);
684
691
  3. иначе спрашивает логин/пароль в терминале (в TTY) и после успешного входа
@@ -696,7 +703,7 @@ zames --headed
696
703
  браузер без окна; `--headed` / `headless: false` показывают его.
697
704
 
698
705
  Headless работает из коробки: headless Chrome обычно представляется как
699
- `HeadlessChrome/...`, и CDN DeepSeek блокирует такой User-Agent ответом 403,
706
+ `HeadlessChrome/...`, и CDN сайта блокирует такой User-Agent ответом 403,
700
707
  поэтому zames срезает этот маркер перед загрузкой страницы (сохраняя реальную
701
708
  версию движка). `--headed` нужен не только ради входа.
702
709
 
@@ -762,7 +769,7 @@ zames
762
769
  Пока агент работает, можно продолжать печатать: Enter ставит сообщение в
763
770
  очередь (оно уйдёт сразу после текущей задачи, в тот же чат), а Esc / Ctrl+C
764
771
  прерывают текущую генерацию. Это повторяет набор текста во время генерации на
765
- сайте DeepSeek.
772
+ сайте чата.
766
773
 
767
774
  ### Строка ввода
768
775
 
@@ -794,8 +801,8 @@ zames
794
801
  или собственной вставкой терминала). zames сохраняет его в `<project>/tmp` и
795
802
  показывает маркер в строке — `[image#1]` для картинок, `[file#1]` для прочих
796
803
  файлов — затем загружает реальный файл в чат вместе с вашим сообщением. Это те
797
- же форматы, что принимает веб-чат DeepSeek (PNG, JPEG, GIF, WEBP, BMP, SVG и
798
- обычные типы документов).
804
+ же форматы, что принимает веб-чат DeepSeek / Qwen (PNG, JPEG, GIF, WEBP, BMP,
805
+ SVG и обычные типы документов).
799
806
 
800
807
  Вставка картинки работает, когда терминал передаёт её как `data:` URL или как
801
808
  base64-блок с узнаваемой сигнатурой изображения. Можно также вставить путь к
@@ -904,8 +911,8 @@ zames --task "..." --output-format jsonl \
904
911
  инструменты (Write/Edit/MultiEdit/ApplyPatch/Bash, GitAdd/GitCommit/GitPush)
905
912
  убраны из набора, чтобы агент изучал код, не трогая дерево. Стартовать в нём
906
913
  можно с `--plan`.
907
- - /compact — попросить DeepSeek сжать текущий чат в передаточное резюме, затем
908
- открыть НОВЫЙ чат, заново отправить системный промпт и положить резюме как
914
+ - /compact — попросить активный бэкенд сжать текущий чат в передаточное резюме,
915
+ затем открыть НОВЫЙ чат, заново отправить системный промпт и положить резюме как
909
916
  перенесённый контекст. Используйте, когда контекст разрастается.
910
917
  - /goal [text|clear] — задать долгоживущую цель сессии. Она добавляется в
911
918
  начало каждого сообщения задачи, поэтому модель держит общую картину через
@@ -1044,31 +1051,35 @@ enum переключаются на месте; числа и строки от
1044
1051
 
1045
1052
  ## FAQ
1046
1053
 
1047
- **Это официальный продукт DeepSeek?**
1048
- Нет. zames управляет публичным веб-интерфейсом chat.deepseek.com через
1049
- настоящий браузер, как обычный пользователь. Он не связан с DeepSeek.
1054
+ **Это официальный продукт DeepSeek или Qwen?**
1055
+ Нет. zames управляет публичными веб-интерфейсами `chat.deepseek.com` /
1056
+ `chat.qwen.ai` через настоящий браузер, как обычный пользователь. Он не связан
1057
+ ни с DeepSeek, ни с Alibaba.
1050
1058
 
1051
1059
  **Нужен ли API-ключ?**
1052
- Нет. Вы один раз входите в свой аккаунт DeepSeek; сессия хранится в
1053
- `~/.zames/profile` и переиспользуется.
1060
+ Нет. Вы один раз входите в свой аккаунт на нужном чате — DeepSeek (хранится в
1061
+ `~/.zames/profile`) или Qwen (хранится в `~/.zames/qwen-profile`) — и сессия
1062
+ переиспользуется. Qwen также работает в гостевом режиме на несколько сообщений.
1054
1063
 
1055
1064
  **Какую модель он использует?**
1056
- Ту, которую использует веб-чат DeepSeek (DeepSeek-V3 или reasoning-модель при
1057
- включённом «Deep thinking»). zames никогда не вызывает API напрямую.
1065
+ Ту, которую использует активный веб-чат. В DeepSeek это DeepSeek-V3 (или
1066
+ reasoning-модель при включённом «Deep thinking»). В Qwen модель можно выбрать
1067
+ командой `/model` (например, `qwen3.7-plus`). zames никогда не вызывает API
1068
+ напрямую.
1058
1069
 
1059
1070
  **Работает ли headless / на сервере?**
1060
1071
  Да — headless по умолчанию, а `--output-format jsonl` делает его скриптуемым.
1061
1072
  `--headed` нужен только для ручного входа или отладки селекторов.
1062
1073
 
1063
1074
  **Разрешена ли автоматизация веб-интерфейса?**
1064
- Это зависит от условий DeepSeek; автоматизация сайта может их нарушать.
1065
- Используйте на свой риск и держите троттлинг отправки (15 с по умолчанию),
1066
- чтобы не бить по рейт-лимиту.
1075
+ Это зависит от условий сайта; автоматизация сайта может их нарушать.
1076
+ Используйте на свой риск и держите троттлинг отправки (15 с в DeepSeek по
1077
+ умолчанию), чтобы не бить по рейт-лимиту.
1067
1078
 
1068
1079
  **Чем отличается от Claude Code / Codex?**
1069
1080
  Та же форма (инструменты, `AGENTS.md`, навыки, MCP, слэш-команды), но работает
1070
- на вашем аккаунте DeepSeek вместо API, как автоматизация браузера, а не
1071
- first-party API-клиент.
1081
+ на вашем аккаунте DeepSeek или Qwen вместо API, как автоматизация браузера, а
1082
+ не first-party API-клиент.
1072
1083
 
1073
1084
  ## Ссылки
1074
1085
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.73.3",
3
+ "version": "2.73.4",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,13 +1,13 @@
1
- // Render docs/how-it-works.png — the architecture diagram used in README.md.
2
- //
3
- // Why a generator: the README must show BOTH chat backends (DeepSeek and
4
- // Qwen), not just DeepSeek. A hand-drawn PNG drifts; this renders a fixed
5
- // HTML page with headless Chromium so the diagram is reproducible and easy to
6
- // update when a provider is added.
1
+ // Render docs/how-it-works.png — the "How it works" flowchart used in
2
+ // README.md. Reproduces the ORIGINAL mermaid `flowchart LR` look (rounded
3
+ // lavender nodes, gray arrows, white background, a simple left-to-right
4
+ // chain) so the diagram stays visually the same as before. The ONLY change:
5
+ // the model node now shows BOTH supported backends (chat.deepseek.com /
6
+ // chat.qwen.ai) instead of only chat.deepseek.com.
7
7
  //
8
8
  // Run: npm run render-how (tsx scripts/render-how-it-works.mts)
9
9
  //
10
- // Isolation: chromium.launch() uses a THROWAWAY in-memory context, never
10
+ // Isolation: chromium.launch() uses a THROWAWAY context, never
11
11
  // ~/.zames/profile (see AGENTS.md — never touch the live agent's browser).
12
12
 
13
13
  import { chromium } from 'playwright'
@@ -17,103 +17,165 @@ import { fileURLToPath } from 'node:url'
17
17
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
18
18
  const out = path.join(root, 'docs', 'how-it-works.png')
19
19
 
20
- const C = {
21
- bg: '#23272e',
22
- panel: '#2b303a',
23
- border: '#3b4250',
24
- text: '#cfc9b0',
25
- dim: '#8a9bb5',
26
- user: '#b4b8d0',
27
- accent: '#7fc4f0',
28
- ds: '#6f8ed0',
29
- qwen: '#6fd0b0',
30
- tool: '#c6a97e',
20
+ // Mermaid default-theme palette (matches the previous diagram).
21
+ const FILL = '#ECECFF'
22
+ const STROKE = '#9370DB'
23
+ const TEXT = '#333333'
24
+ const FONT = 'trebuchet ms, verdana, arial, sans-serif'
25
+
26
+ const W = 1120
27
+ const H = 150
28
+ const NODE_W = 250
29
+ const NODE_H = 70
30
+ const CY = H / 2
31
+ const PAD = 30
32
+
33
+ // A left-to-right chain with the SAME three steps as the original diagram;
34
+ // only the last node gained a second line (the two supported backends).
35
+ interface Node {
36
+ x: number
37
+ w: number
38
+ lines: string[]
31
39
  }
32
40
 
33
- const W = 1300
34
- const H = 360
41
+ const nodes: Node[] = [
42
+ { x: PAD, w: NODE_W, lines: ['terminal (you)'] },
43
+ {
44
+ x: PAD + NODE_W + 150,
45
+ w: 260,
46
+ lines: ['zames', 'parse tool-call · run tool'],
47
+ },
48
+ {
49
+ x: PAD + NODE_W + 150 + 260 + 150,
50
+ w: 260,
51
+ lines: ['chat.deepseek.com', 'chat.qwen.ai'],
52
+ },
53
+ ]
35
54
 
36
- const box = (
37
- title: string,
38
- sub: string,
39
- color: string,
40
- ): string =>
41
- '<div class="box" style="border-color:' +
42
- color +
43
- '">' +
44
- '<div class="box-t" style="color:' +
45
- color +
46
- '">' +
47
- title +
48
- '</div>' +
49
- '<div class="box-s">' +
50
- sub +
51
- '</div></div>'
55
+ const esc = (s: string): string =>
56
+ s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
52
57
 
53
- const html =
54
- '<!doctype html><html><head><meta charset="utf-8"><style>' +
55
- '@font-face{font-family:ZM;src:local("DejaVu Sans Mono")}' +
56
- 'html,body{margin:0;padding:0;background:' +
57
- C.bg +
58
- '}' +
59
- 'body{font-family:"DejaVu Sans Mono",monospace;color:' +
60
- C.text +
61
- ';-webkit-font-smoothing:antialiased}' +
62
- '.wrap{box-sizing:border-box;width:' +
58
+ const nodeSvg = (n: Node): string => {
59
+ const y = CY - NODE_H / 2
60
+ const rx = 12
61
+ const lh = 20
62
+ const startY = CY - ((n.lines.length - 1) * lh) / 2 + 5
63
+ const cx = n.x + n.w / 2
64
+ const texts = n.lines
65
+ .map(
66
+ (t, i) =>
67
+ '<text x="' +
68
+ cx +
69
+ '" y="' +
70
+ (startY + i * lh) +
71
+ '" text-anchor="middle" font-family="' +
72
+ FONT +
73
+ '" font-size="15" fill="' +
74
+ TEXT +
75
+ '">' +
76
+ esc(t) +
77
+ '</text>',
78
+ )
79
+ .join('')
80
+ return (
81
+ '<rect x="' +
82
+ n.x +
83
+ '" y="' +
84
+ y +
85
+ '" width="' +
86
+ n.w +
87
+ '" height="' +
88
+ NODE_H +
89
+ '" rx="' +
90
+ rx +
91
+ '" ry="' +
92
+ rx +
93
+ '" fill="' +
94
+ FILL +
95
+ '" stroke="' +
96
+ STROKE +
97
+ '" stroke-width="2"/>' +
98
+ texts
99
+ )
100
+ }
101
+
102
+ // A horizontal edge from x1 to x2 at height y, with a centered label above it.
103
+ const edge = (
104
+ x1: number,
105
+ x2: number,
106
+ y: number,
107
+ label: string,
108
+ dir: 'r' | 'l',
109
+ ): string => {
110
+ const mid = (x1 + x2) / 2
111
+ const marker = dir === 'r' ? 'ar' : 'al'
112
+ return (
113
+ '<path d="M' +
114
+ x1 +
115
+ ' ' +
116
+ y +
117
+ ' L' +
118
+ x2 +
119
+ ' ' +
120
+ y +
121
+ '" fill="none" stroke="' +
122
+ TEXT +
123
+ '" stroke-width="1.6" marker-end="url(#' +
124
+ marker +
125
+ ')"/>' +
126
+ '<text x="' +
127
+ mid +
128
+ '" y="' +
129
+ (y - 8) +
130
+ '" text-anchor="middle" font-family="' +
131
+ FONT +
132
+ '" font-size="12" fill="' +
133
+ TEXT +
134
+ '">' +
135
+ esc(label) +
136
+ '</text>'
137
+ )
138
+ }
139
+
140
+ const a = nodes[0]
141
+ const z = nodes[1]
142
+ const m = nodes[2]
143
+
144
+ const reqY = CY - 16
145
+ const ansY = CY + 20
146
+
147
+ const svg =
148
+ '<svg xmlns="http://www.w3.org/2000/svg" width="' +
149
+ W +
150
+ '" height="' +
151
+ H +
152
+ '" viewBox="0 0 ' +
63
153
  W +
64
- 'px;height:' +
154
+ ' ' +
65
155
  H +
66
- 'px;padding:26px 30px}' +
67
- '.row{display:flex;align-items:center;justify-content:space-between;gap:8px}' +
68
- '.box{box-sizing:border-box;border:2px solid;border-radius:10px;padding:14px 16px;' +
69
- 'background:' +
70
- C.panel +
71
- ';min-width:150px;text-align:center}' +
72
- '.box-t{font-size:18px;font-weight:700;margin-bottom:6px}' +
73
- '.box-s{font-size:12px;color:' +
74
- C.dim +
75
- ';line-height:1.5}' +
76
- '.arrow{font-size:26px;color:' +
77
- C.dim +
78
- '}' +
79
- '.flow-label{font-size:12px;color:' +
80
- C.dim +
81
- ';text-align:center;margin-top:4px}' +
82
- '.providers{display:flex;flex-direction:column;gap:10px}' +
83
- '.col{display:flex;flex-direction:column;align-items:center;gap:4px}' +
84
- '.legend{margin-top:20px;font-size:12px;color:' +
85
- C.dim +
86
- ';text-align:center}' +
87
- '.legend b{color:' +
88
- C.text +
89
- '}' +
90
- '</style></head><body><div class="wrap">' +
91
- '<div class="row">' +
92
- // left: the operator
93
- '<div class="col">' +
94
- box('Operator', 'terminal task<br/>+ tool approval', C.user) +
95
- '<div class="flow-label">types a task</div>' +
96
- '</div>' +
97
- '<div class="arrow">&#8594;</div>' +
98
- // middle: zames
99
- '<div class="col">' +
100
- box(
101
- 'zames',
102
- 'sandboxed tools<br/>parses tool-calls<br/>runs them locally',
103
- C.accent,
104
- ) +
105
- '<div class="flow-label">system prompt + task</div>' +
106
- '</div>' +
107
- '<div class="arrow">&#8644;</div>' +
108
- // right: the two backends
109
- '<div class="providers">' +
110
- box('DeepSeek', 'chat.deepseek.com<br/>~/.zames/profile', C.ds) +
111
- box('Qwen', 'chat.qwen.ai<br/>~/.zames/qwen-profile', C.qwen) +
112
- '</div>' +
113
- '</div>' +
114
- '<div class="legend">zames drives a <b>real browser</b> over a persistent per-provider profile &mdash; no API key. ' +
115
- 'Switch live with <b>/provider &lt;deepseek|qwen&gt;</b> (each chat id belongs to one backend).</div>' +
116
- '</div></body></html>'
156
+ '">' +
157
+ '<defs>' +
158
+ '<marker id="ar" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L9,3 L0,6 z" fill="' +
159
+ TEXT +
160
+ '"/></marker>' +
161
+ '<marker id="al" markerWidth="10" markerHeight="10" refX="1" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M9,0 L0,3 L9,6 z" fill="' +
162
+ TEXT +
163
+ '"/></marker>' +
164
+ '</defs>' +
165
+ '<rect width="100%" height="100%" fill="#ffffff"/>' +
166
+ // request edges (top, left -> right)
167
+ edge(a.x + a.w, z.x, reqY, 'task', 'r') +
168
+ edge(z.x + z.w, m.x, reqY, 'system prompt + task', 'r') +
169
+ // answer edges (bottom, right -> left)
170
+ edge(m.x, z.x + z.w, ansY, 'model answers (raw SSE)', 'l') +
171
+ edge(z.x, a.x + a.w, ansY, 'answer', 'l') +
172
+ nodes.map(nodeSvg).join('') +
173
+ '</svg>'
174
+
175
+ const html =
176
+ '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:#fff}svg{display:block}</style></head><body>' +
177
+ svg +
178
+ '</body></html>'
117
179
 
118
180
  const browser = await chromium.launch({ headless: true })
119
181
  try {
@@ -122,7 +184,8 @@ try {
122
184
  deviceScaleFactor: 2,
123
185
  })
124
186
  await page.setContent(html, { waitUntil: 'load' })
125
- await page.screenshot({ path: out })
187
+ const el = await page.$('svg')
188
+ await (el || page).screenshot({ path: out })
126
189
  console.log('wrote', path.relative(root, out))
127
190
  } finally {
128
191
  await browser.close()