zames_pro 2.95.3 → 2.95.5

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,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.95.5] - 2026-11-05
11
+
12
+ ### Added
13
+
14
+ - **site:** a 404 page, hreflang alternates and cache-busted assets
15
+
16
+ ### Fixed
17
+
18
+ - **tui:** keep the AskUser question and answer in the log
19
+ - **changelog:** keep a manual Unreleased note inside its group
20
+ - **site:** stop the landing title repeating the site name in og:title
21
+ - **site:** give the social preview its own static image
22
+
23
+ ### Changed
24
+
25
+ - **tui:** paint the AskUser question in the tool color
26
+ - drop dead provider-unification modules and stale doc text
27
+ - **site:** a deeper palette, bigger type and calmer cards
28
+
29
+ ## [2.95.4] - 2026-11-05
30
+
31
+ ### Fixed
32
+
33
+ - **site:** stop the copy buttons from covering or scrolling away
34
+ - **site:** give the hero copy button its shared look and drop demo artifacts
35
+
10
36
  ## [2.95.3] - 2026-11-04
11
37
 
12
38
  ### Fixed
package/README.md CHANGED
@@ -38,7 +38,7 @@ lock-in. Install with `npm install -g zames_pro`, run `zames` in any project
38
38
  folder, and describe the task in plain language.
39
39
 
40
40
  <p align="center">
41
- <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
41
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.gif" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
42
42
  </p>
43
43
 
44
44
  ## Quick start
@@ -500,7 +500,7 @@ Qwen или GLM) в агента для командной строки в ду
500
500
  можно описать обычным языком.
501
501
 
502
502
  <p align="center">
503
- <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="сессия zames в терминале — терминальный coding-агент и AI-ассистент программиста без API-ключа" width="860">
503
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.gif" alt="сессия zames в терминале — терминальный coding-агент и AI-ассистент программиста без API-ключа" width="860">
504
504
  </p>
505
505
 
506
506
  ## Быстрый старт
package/dist/run-task.js CHANGED
@@ -2,8 +2,35 @@ import { theme } from './theme.js';
2
2
  import { withGoal, mergeMessages, isSlashCommand, formatDuration, } from './commands.js';
3
3
  import { buildEvaluatorPrompt, buildContinuationDirective, parseGoalVerdict, GOAL_MAX_TURNS, } from './goal.js';
4
4
  import { formatCompactTokens, pasteReplacement, expandPastes } from './input.js';
5
- import { DEFAULT_LOCALE } from './i18n.js';
5
+ import { DEFAULT_LOCALE, translate } from './i18n.js';
6
6
  import { providerAskDeadlineMs } from './config.js';
7
+ // The two log lines an `AskUser` turn leaves behind: the question with its
8
+ // options (the recommended one starred) and the operator's answer. PURE, so the
9
+ // wording is unit-tested without a live modal; `runTask` prints them through
10
+ // the editor after the modal closes (the modal's own frame is erased on
11
+ // unmount, and without these lines the question would leave no trace in the
12
+ // terminal or the chat history).
13
+ const ASK_NL = String.fromCharCode(10);
14
+ export function formatAskUserLog(question, options, recommended, answer, t) {
15
+ const qLines = question
16
+ .split(ASK_NL)
17
+ .map((l) => ' ' + l.trim())
18
+ .join(ASK_NL);
19
+ const optLines = options
20
+ .map((o, i) => {
21
+ const mark = i === recommended ? '* ' : '- ';
22
+ const tag = i === recommended ? ' (' + t('askuser.recommended') + ')' : '';
23
+ return ' ' + mark + o + tag;
24
+ })
25
+ .join(ASK_NL);
26
+ return {
27
+ question: '◆ ' + t('askuser.title') + ASK_NL + qLines + ASK_NL + optLines,
28
+ answer: '❯ ' +
29
+ t('askuser.your_answer') +
30
+ ' ' +
31
+ (answer === null || answer === '' ? t('askuser.hint_dismiss') : answer),
32
+ };
33
+ }
7
34
  // Ask the operator to approve a tool call flagged by an `ask` permission rule
8
35
  // While the LineEditor owns the terminal it must be paused first, or the
9
36
  // confirmation prompt and the input line fight for the same keys. A non-TTY run
@@ -429,18 +456,34 @@ export async function runTask(deps, browser, tools, taskText, workdir, opts, att
429
456
  ? async (req) => {
430
457
  if (!editor)
431
458
  return null;
459
+ // The modal is a SEPARATE Ink app: its frame is erased when it
460
+ // unmounts, so without this the question and the operator's
461
+ // answer leave no trace — neither in the terminal nor in the
462
+ // chat history. Print them as a normal log turn instead.
463
+ const tr = translate(locale);
432
464
  editor.pause();
465
+ let answer = null;
433
466
  try {
434
467
  const { askQuestionInk } = await import('./tui/run-ask.js');
435
- return await askQuestionInk({
468
+ answer = await askQuestionInk({
436
469
  question: req.question,
437
470
  options: req.options,
438
471
  recommended: req.recommended,
439
472
  allowText: req.allowText,
440
473
  });
474
+ return answer;
441
475
  }
442
476
  finally {
477
+ // Print AFTER resume, never before pause: on the alternate
478
+ // screen the modal unmount wipes everything painted into the
479
+ // old app, so the question and the answer would vanish.
443
480
  editor.resume();
481
+ const logged = formatAskUserLog(req.question, req.options, req.recommended, answer, tr);
482
+ // The question block is painted in the TOOL color (the warm
483
+ // one tool calls use) so it reads as a distinct event, not
484
+ // as another grey service line next to the agent's answers.
485
+ editor.printAbove(theme.tool(logged.question));
486
+ editor.printAbove(theme.user(logged.answer));
444
487
  }
445
488
  }
446
489
  : onAskUser,
package/llms-full.txt CHANGED
@@ -23,7 +23,7 @@ lock-in. Install with `npm install -g zames_pro`, run `zames` in any project
23
23
  folder, and describe the task in plain language.
24
24
 
25
25
  <p align="center">
26
- <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.png" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
26
+ <img src="https://raw.githubusercontent.com/Viqto0r/zames_pro/master/docs/demo.gif" alt="zames terminal coding agent session — an AI coding agent working in the command line without an API key" width="860">
27
27
  </p>
28
28
 
29
29
  ## Quick start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.95.3",
3
+ "version": "2.95.5",
4
4
  "description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen or GLM — 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": {
@@ -40,13 +40,14 @@
40
40
  "qwen-smoke": "tsx scripts/qwen-smoke.mts",
41
41
  "qwen-models-smoke": "tsx scripts/qwen-models-smoke.mts",
42
42
  "sched-smoke": "tsx scripts/sched-smoke.mts",
43
- "render-demo": "tsx scripts/render-demo.mts",
43
+ "demo:replay": "tsx scripts/demo-replay.mts",
44
44
  "render-how": "tsx scripts/render-how-it-works.mts",
45
45
  "build:readme": "tsx scripts/build-readme.mts",
46
46
  "build:llms": "tsx scripts/build-llms.mts",
47
47
  "postinstall": "node scripts/postinstall.mjs",
48
48
  "prepare": "husky",
49
49
  "glm-smoke": "tsx scripts/glm-smoke.mts",
50
+ "glm-warmup": "tsx scripts/glm-warmup.mts",
50
51
  "seo:topics": "tsx scripts/seo-topics.mts",
51
52
  "ui-smoke": "tsx scripts/ui-smoke.mts"
52
53
  },
package/dist/ask-retry.js DELETED
@@ -1,10 +0,0 @@
1
- // N1 stage 4: the retry POLICY of a provider's ask() as a PURE function.
2
- //
3
- // The real implementation now lives in src/retry-policy.ts (it grew the
4
- // incremental-backoff mode); this module stays as the historical import
5
- // point so the providers and the tests do not churn.
6
- //
7
- // WHY the split: the DECISION (family -> wait / compact / give-up) is pure
8
- // and unit-tested, while the loop (sends, throttling, aborts) stays in the
9
- // browser class. A new backend only supplies the `classify` function.
10
- export { decideRetry } from './retry-policy.js';
package/dist/ask-state.js DELETED
@@ -1,80 +0,0 @@
1
- // N3: the PHASE DECISION of DeepSeek's `_askOnce()` as a pure state machine.
2
- //
3
- // `_askOnce` (src/browser.ts) is the most fragile function of the project: a
4
- // start-wait loop, a finish loop, the Continue button, the stability window
5
- // and four detectors (rate limit / context limit / truncated / finished
6
- // without answer). Until now it was covered only by live smoke runs, and a
7
- // wrong ordering of the checks kept reappearing (the "Stopped" hang, the
8
- // "agent stopped with a Continue button", the 15-minute spinner on a dead
9
- // turn). The Playwright part keeps COLLECTING an observation; the TRANSITION
10
- // below is pure and unit-tested against the real cases from DESIGN-NOTES.
11
- /** Grace ticks before a FINISHED-with-answer turn is returned (default 2). */
12
- export const NET_FINISHED_TICKS = 2;
13
- /** Ticks a dead (FINISHED, no answer, no Continue) turn waits before giving up. */
14
- export const DEAD_TURN_TICKS = 6;
15
- /**
16
- * One transition of the ask() state machine.
17
- *
18
- * ORDER MATTERS and is the whole point of this function:
19
- * 1. an abort wins over everything (the operator stopped);
20
- * 2. the CONTEXT limit is checked BEFORE the rate limit — both look like a
21
- * toast, and retrying an exhausted chat is useless;
22
- * 3. rate limit / server busy are retryable errors;
23
- * 4. a truncated turn PREFERS the Continue button (it RESUMES the turn) and
24
- * only falls back to a resend when there is no button;
25
- * 5. a PAUSED generation (`continueVisible`) is never "settled": the
26
- * stability early-return must be checked AFTER it, otherwise a partial
27
- * answer is returned with Continue still on screen.
28
- */
29
- export function nextPhase(phase, o, opts) {
30
- if (o.aborted)
31
- return { kind: 'return', text: 'dom' };
32
- if (o.contextLimit)
33
- return { kind: 'throw', error: 'context-limit' };
34
- if (o.rateLimited)
35
- return { kind: 'throw', error: 'rate-limit' };
36
- if (o.serverBusy)
37
- return { kind: 'throw', error: 'server-busy' };
38
- // A truncated / answer-less turn: click Continue when possible (it resumes
39
- // the SAME turn); otherwise resend.
40
- if (o.truncated || o.noAnswer) {
41
- if (opts.autoContinue && o.continueVisible) {
42
- return { kind: 'click-continue' };
43
- }
44
- return { kind: 'throw', error: 'incomplete' };
45
- }
46
- if (phase === 'start') {
47
- if (o.isNew)
48
- return { kind: 'started' };
49
- if (o.hasText && o.notGenerating && !o.continueVisible) {
50
- return { kind: 'return', text: 'dom' };
51
- }
52
- // A reasoning pause during the THINK phase: click Continue so the answer
53
- // starts flowing instead of waiting for the start deadline.
54
- if (opts.autoContinue && opts.deepThinking && o.continueVisible) {
55
- return { kind: 'click-continue' };
56
- }
57
- return { kind: 'wait' };
58
- }
59
- // --- finish loop ---------------------------------------------------------
60
- // The SSE body is ground truth: FINISHED with an answer, after a short grace
61
- // window, is complete.
62
- if (o.finishedWithAnswer && opts.netFinishedTicks >= NET_FINISHED_TICKS) {
63
- return { kind: 'return', text: 'net' };
64
- }
65
- // A dead turn (the server ended it with nothing and there is no Continue to
66
- // resume): give up quickly so ask() resends instead of a long spinner.
67
- if (opts.deadTicks >= DEAD_TURN_TICKS && !o.continueVisible) {
68
- return { kind: 'throw', error: 'incomplete' };
69
- }
70
- // A PAUSED generation looks stable: check it BEFORE the stability return.
71
- if (opts.autoContinue && opts.deepThinking && o.continueVisible) {
72
- return { kind: 'click-continue' };
73
- }
74
- if (o.isNew && opts.stableReached) {
75
- if (o.notGenerating)
76
- return { kind: 'return', text: 'dom' };
77
- return { kind: 'wait' };
78
- }
79
- return { kind: 'wait' };
80
- }
@@ -1,69 +0,0 @@
1
- // N1 stage 1: the helpers that were byte-identical in DeepSeekBrowser,
2
- // QwenBrowser and GlmBrowser. Composition, not inheritance: every provider
3
- // class owns a page and calls these with it, so the provider-specific logic
4
- // stays where it is and only the shared mechanics live here.
5
- /** Best-effort: a page error while marking the tab title must never break a
6
- * send loop, so every failure is swallowed. */
7
- export async function setBusyTitle(page, busy, setInDom) {
8
- try {
9
- await page.evaluate(setInDom, busy);
10
- }
11
- catch { }
12
- }
13
- /**
14
- * The first selector that has a VISIBLE element. `timeout` is a per-selector
15
- * budget, not a total one (the providers rely on that: a login form is tried
16
- * with a long budget while optional probes use a short one).
17
- *
18
- * Two probe styles exist in the providers (`waitFor({state:'visible'})` in
19
- * DeepSeek/Qwen and `count()` + `isVisible()` in GLM). `style: 'wait'` keeps
20
- * the first (it also covers a late-mounting element); `style: 'check'` keeps
21
- * the second (no waiting state, a cheap poll).
22
- */
23
- export async function findVisible(page, selectors, timeout = 1000, style = 'wait') {
24
- for (const sel of selectors) {
25
- const loc = page.locator(sel).last();
26
- if (style === 'wait') {
27
- try {
28
- await loc.waitFor({ state: 'visible', timeout });
29
- return loc;
30
- }
31
- catch { }
32
- continue;
33
- }
34
- try {
35
- if ((await loc.count()) === 0)
36
- continue;
37
- if (await loc.isVisible({ timeout }).catch(() => false))
38
- return loc;
39
- }
40
- catch { }
41
- }
42
- return null;
43
- }
44
- /**
45
- * Sleep in 100 ms chunks so Esc and the ask() watchdog stay responsive.
46
- * Returns true when the wait was INTERRUPTED (abort / cancelled ask), so the
47
- * caller can stop early. `onTick` fires about once a second with the time
48
- * left — that is what animates the throttle countdown.
49
- */
50
- export async function sleepInterruptible(page, ms, isInterrupted, onTick) {
51
- const step = 100;
52
- let left = ms;
53
- let sinceTick = 0;
54
- while (left > 0) {
55
- if (isInterrupted())
56
- return true;
57
- const chunk = Math.min(step, left);
58
- await page.waitForTimeout(chunk);
59
- left -= chunk;
60
- if (onTick) {
61
- sinceTick += chunk;
62
- if (sinceTick >= 1000) {
63
- sinceTick = 0;
64
- onTick(left);
65
- }
66
- }
67
- }
68
- return isInterrupted();
69
- }
@@ -1,53 +0,0 @@
1
- // N1 stage 2: the error families shared by the chat providers.
2
- //
3
- // Each provider used to declare its own copy (`QwenRateLimitError`,
4
- // `GlmRateLimitError`, ...), so the retry policy in every `ask()` re-listed the
5
- // same six families and a new one had to be added three times. The classes
6
- // here are the SHARED vocabulary; the provider keeps its own pre-check
7
- // (`instanceof`) only where the semantics genuinely differ (guest limits and
8
- // the age gate exist on one backend only).
9
- //
10
- // `provider` is carried for the operator-facing message only — the retry
11
- // decision is per CLASS, never per string.
12
- export class ProviderRateLimitError extends Error {
13
- constructor(detail) {
14
- super('rate limit: ' + detail);
15
- this.name = 'ProviderRateLimitError';
16
- }
17
- }
18
- export class ProviderServerBusyError extends Error {
19
- constructor(detail) {
20
- super('server busy: ' + detail);
21
- this.name = 'ProviderServerBusyError';
22
- }
23
- }
24
- export class ProviderContextLimitError extends Error {
25
- constructor(detail) {
26
- super('context limit: ' + detail);
27
- this.name = 'ProviderContextLimitError';
28
- }
29
- }
30
- export class ProviderIncompleteError extends Error {
31
- constructor(detail) {
32
- super('generation incomplete: ' + detail);
33
- this.name = 'ProviderIncompleteError';
34
- }
35
- }
36
- export class ProviderGuestLimitError extends Error {
37
- constructor(detail) {
38
- super('guest limit: ' + detail);
39
- this.name = 'ProviderGuestLimitError';
40
- }
41
- }
42
- export class ProviderAgeGateError extends Error {
43
- constructor(detail) {
44
- super('age gate: ' + detail);
45
- this.name = 'ProviderAgeGateError';
46
- }
47
- }
48
- export class ProviderCaptchaError extends Error {
49
- constructor(detail) {
50
- super('captcha: ' + detail);
51
- this.name = 'ProviderCaptchaError';
52
- }
53
- }
@@ -1,98 +0,0 @@
1
- // The retry POLICY of a provider's ask() as a PURE function.
2
- //
3
- // WHY: the three providers ran the same loop — call _askOnce, classify the
4
- // thrown error into a family, retry that family within its own budget — and
5
- // each copy had its own order of checks and its own wording. The loop itself
6
- // (sends, throttling, aborts) stays in the browser class; only the DECISION is
7
- // here, so every branch is unit-tested without a browser.
8
- //
9
- // Incremental retries (default): the three transient families (and the
10
- // agent-loop watchdog) no longer count their attempts; the WAIT grows from the
11
- // family's base (rate limit: 5m, busy: 3s, incomplete: 2s) up to the ceiling
12
- // (browser.maxRetryWaitMs, default 30m), and the only way out is a cancel
13
- // (Esc/Ctrl+C) or a FATAL error (bad login, captcha, guest limit) — the same
14
- // place it was before.
15
- //
16
- // One thing matters for understanding this module: a retry that waits the
17
- // SAME fixed pause forever is not endless, it is a busy loop that never lets
18
- // the block expire. The escalation is what makes "infinite" safe.
19
- import { backoffWaitMs, canRetry, } from './retry-backoff.js';
20
- /**
21
- * Decide what to do after a failed attempt.
22
- *
23
- * classify maps the thrown error to a family; it lives in the provider
24
- * because the backend-specific markers (guest limit, age gate, captcha) are
25
- * provider knowledge. A null classification falls through to the generic
26
- * attempt budget.
27
- *
28
- * backoff carries the retry mode (infiniteRetries, the ceiling) — see
29
- * src/retry-backoff.ts.
30
- */
31
- export function decideRetry(family, state, budgets, waits, backoff, opts = {}) {
32
- // Strict === true: the infinite mode is opt-in by an EXPLICIT config field.
33
- // A caller that omits it keeps the historical counted budget, so a
34
- // partially built config can never silently retry forever.
35
- const infinite = backoff?.infiniteRetries === true; // strict
36
- const cfg = {
37
- infiniteRetries: infinite,
38
- maxRetries: backoff?.maxRetries ?? 6,
39
- baseWaitMs: waits.rateLimitMs,
40
- maxWaitMs: backoff?.maxWaitMs ?? 0,
41
- };
42
- // The cause of a transient failure decides the FIRST wait — a rate block is
43
- // waited out for minutes, a transient busy notice for seconds — and the
44
- // escalation adds to it from there.
45
- const baseWait = (f) => f === 'server-busy'
46
- ? waits.serverBusyMs
47
- : f === 'incomplete'
48
- ? waits.incompleteMs
49
- : waits.rateLimitMs;
50
- const waitFor = (f, attempts) => backoffWaitMs({ ...cfg, baseWaitMs: baseWait(f) }, attempts, {
51
- // A server Retry-After is the best clock we have; it is still clamped
52
- // to the ceiling and never shortens the escalation.
53
- serverWaitMs: f === 'rate-limit' ? waits.retryAfterMs : undefined,
54
- jitter: opts.jitter,
55
- });
56
- switch (family) {
57
- case 'fatal':
58
- return { kind: 'give-up', family };
59
- case 'context-limit':
60
- if (state.contextLimitRetries >= budgets.maxContextLimitRetries) {
61
- return { kind: 'give-up', family };
62
- }
63
- return { kind: 'compact', family };
64
- case 'rate-limit':
65
- if (!canRetry(cfg, state.rateLimitRetries)) {
66
- return { kind: 'give-up', family };
67
- }
68
- return {
69
- kind: 'wait',
70
- family,
71
- waitMs: waitFor('rate-limit', state.rateLimitRetries + 1),
72
- };
73
- case 'server-busy':
74
- if (!canRetry(cfg, state.serverBusyRetries)) {
75
- return { kind: 'give-up', family };
76
- }
77
- return {
78
- kind: 'wait',
79
- family,
80
- waitMs: waitFor('server-busy', state.serverBusyRetries + 1),
81
- };
82
- case 'incomplete':
83
- if (!canRetry(cfg, state.incompleteRetries)) {
84
- return { kind: 'give-up', family };
85
- }
86
- return {
87
- kind: 'wait',
88
- family,
89
- waitMs: waitFor('incomplete', state.incompleteRetries + 1),
90
- };
91
- default:
92
- // A generic failure is retried while the TOTAL attempt budget lasts.
93
- if (state.attempt >= budgets.askRetries - 1) {
94
- return { kind: 'give-up', family: 'generic' };
95
- }
96
- return { kind: 'generic', waitMs: 2000 * (state.attempt + 1) };
97
- }
98
- }
package/dist/send-slot.js DELETED
@@ -1,36 +0,0 @@
1
- // N1 stage 3: the SEND SLOT — the pause before a send that keeps the chat's
2
- // rate limit happy.
3
- //
4
- // Every provider had its own `_waitForSendSlot()` with the same shape (skip a
5
- // non-agent send, compute the remaining gap, report the animated countdown,
6
- // mark the state 'paused', sleep interruptibly) and each computed the gap its
7
- // own way (DeepSeek: interval + thinking bonus; Qwen: interval; GLM: the
8
- // adaptive sendRuntime). The MECHANICS are shared here; the provider injects
9
- // the gap and the plumbing.
10
- /**
11
- * Wait for the send slot. Returns true when the wait was INTERRUPTED (the
12
- * caller must then abort the send).
13
- */
14
- export async function waitForSendSlot(agent, deps) {
15
- if (!agent)
16
- return false;
17
- const gap = deps.gapMs();
18
- if (gap <= 0)
19
- return false;
20
- const report = (leftMs) => {
21
- const secs = Math.max(0, Math.ceil(leftMs / 1000));
22
- if (deps.onPause) {
23
- try {
24
- deps.onPause(secs);
25
- }
26
- catch { }
27
- }
28
- else if (deps.onPauseText) {
29
- deps.onPauseText(secs);
30
- }
31
- };
32
- report(gap);
33
- if (deps.emitState)
34
- deps.emitState('paused');
35
- return await deps.sleep(gap, report);
36
- }