zames_pro 2.72.9 → 2.73.1
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 +79 -0
- package/README.md +111 -19
- package/dist/agent-loop.js +17 -5
- package/dist/browser-pure.js +6 -6
- package/dist/browser.js +5 -1
- package/dist/commands.js +7 -0
- package/dist/config.js +56 -1
- package/dist/i18n/common.js +2 -2
- package/dist/i18n/config.js +14 -0
- package/dist/i18n/deepseek.js +26 -26
- package/dist/i18n/help.js +8 -0
- package/dist/i18n/qwen.js +132 -0
- package/dist/i18n.js +2 -0
- package/dist/index.js +295 -28
- package/dist/input.js +23 -0
- package/dist/provider-handoff.js +73 -0
- package/dist/providers.js +50 -0
- package/dist/qwen-browser.js +1743 -0
- package/dist/qwen-chats.js +183 -0
- package/dist/qwen-net-capture.js +221 -0
- package/dist/qwen-ui.js +160 -0
- package/dist/run-task.js +2 -1
- package/dist/sessions.js +15 -4
- package/dist/ui-health.js +27 -21
- package/package.json +8 -3
- package/scripts/coverage-gate.mts +5 -1
- package/scripts/qwen-smoke.mts +189 -0
package/dist/ui-health.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
// UI health check — a cheap "can the agent still drive this page?" probe.
|
|
2
2
|
//
|
|
3
|
-
// WHY: every selector in
|
|
4
|
-
// not control. When
|
|
5
|
-
// task hanging on "input field not found" (or a silently mis-clicked
|
|
6
|
-
// minutes into a run. This module turns that into an explicit answer
|
|
7
|
-
// the task: it checks the capabilities the agent ALWAYS needs and
|
|
8
|
-
// missing ones. The page object is injected (the browser owns
|
|
9
|
-
// the decision logic here stays pure and unit-testable.
|
|
3
|
+
// WHY: every selector in the provider's *-ui.ts is an assumption about a page
|
|
4
|
+
// we do not control. When the site ships a redesign, the FIRST symptom used to
|
|
5
|
+
// be a task hanging on "input field not found" (or a silently mis-clicked
|
|
6
|
+
// send), minutes into a run. This module turns that into an explicit answer
|
|
7
|
+
// BEFORE the task: it checks the capabilities the agent ALWAYS needs and
|
|
8
|
+
// reports the missing ones. The page object is injected (the browser owns
|
|
9
|
+
// Playwright), so the decision logic here stays pure and unit-testable.
|
|
10
10
|
//
|
|
11
11
|
// Only ALWAYS-PRESENT capabilities are probed. The Stop button and the answer
|
|
12
12
|
// nodes are TRANSIENT (Stop exists only during generation, answers only after
|
|
13
13
|
// a turn), so a presence probe on a healthy idle page would report false
|
|
14
14
|
// negatives; they are deliberately excluded.
|
|
15
15
|
import { INPUT_SELECTORS, SEND_SELECTORS, TOGGLE_SELECTORS, NEW_CHAT_SELECTOR, NEW_CHAT_TEXT_RE, } from './deepseek-ui.js';
|
|
16
|
-
// Critical: without the input the prompt
|
|
17
|
-
// cannot be submitted — a task would hang on
|
|
18
|
-
// toggles/newChat degrade gracefully (the toggles
|
|
19
|
-
// state, newChat falls back to navigating to the
|
|
16
|
+
// DeepSeek's default capability list. Critical: without the input the prompt
|
|
17
|
+
// cannot be typed, without send it cannot be submitted — a task would hang on
|
|
18
|
+
// `ds.input_missing`. Advisory: toggles/newChat degrade gracefully (the toggles
|
|
19
|
+
// fall back to the configured state, newChat falls back to navigating to the
|
|
20
|
+
// base URL).
|
|
20
21
|
export const UI_CAPABILITIES = [
|
|
21
22
|
{ capability: 'input', selectors: INPUT_SELECTORS, critical: true },
|
|
22
23
|
{ capability: 'send', selectors: SEND_SELECTORS, critical: true },
|
|
23
24
|
{ capability: 'toggles', selectors: TOGGLE_SELECTORS, critical: false },
|
|
24
|
-
{
|
|
25
|
+
{
|
|
26
|
+
capability: 'newChat',
|
|
27
|
+
selectors: [NEW_CHAT_SELECTOR],
|
|
28
|
+
critical: false,
|
|
29
|
+
textRe: NEW_CHAT_TEXT_RE,
|
|
30
|
+
},
|
|
25
31
|
];
|
|
26
32
|
async function hasAny(page, sel, textRe) {
|
|
27
33
|
try {
|
|
@@ -35,21 +41,21 @@ async function hasAny(page, sel, textRe) {
|
|
|
35
41
|
}
|
|
36
42
|
}
|
|
37
43
|
/**
|
|
38
|
-
* Probe the always-present
|
|
39
|
-
* reported as every capability missing, which the caller surfaces
|
|
40
|
-
* operator instead of letting the next task hang.
|
|
44
|
+
* Probe the always-present controls of a provider's chat page. Never throws: a
|
|
45
|
+
* dead page is reported as every capability missing, which the caller surfaces
|
|
46
|
+
* to the operator instead of letting the next task hang.
|
|
47
|
+
*
|
|
48
|
+
* `capabilities` is a parameter so each provider (deepseek-ui.ts, qwen-ui.ts)
|
|
49
|
+
* can supply its own selectors while the probe logic stays shared.
|
|
41
50
|
*/
|
|
42
|
-
export async function probeUiHealth(page) {
|
|
51
|
+
export async function probeUiHealth(page, capabilities = UI_CAPABILITIES) {
|
|
43
52
|
const present = [];
|
|
44
53
|
const missing = [];
|
|
45
54
|
const missingCritical = [];
|
|
46
|
-
for (const spec of
|
|
55
|
+
for (const spec of capabilities) {
|
|
47
56
|
let found = false;
|
|
48
|
-
// The new-chat control is matched by an EXACT text label on a broad
|
|
49
|
-
// element set (it is a <div> on the current build, not a <button>).
|
|
50
|
-
const textRe = spec.capability === 'newChat' ? NEW_CHAT_TEXT_RE : undefined;
|
|
51
57
|
for (const sel of spec.selectors) {
|
|
52
|
-
if (await hasAny(page, sel, textRe)) {
|
|
58
|
+
if (await hasAny(page, sel, spec.textRe)) {
|
|
53
59
|
found = true;
|
|
54
60
|
break;
|
|
55
61
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Terminal coding agent that drives chat
|
|
3
|
+
"version": "2.73.1",
|
|
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": {
|
|
7
7
|
"zames": "dist/index.js"
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"test": "tsx --test test/*.test.ts",
|
|
34
34
|
"test:watch": "tsx --test --watch test/*.test.ts",
|
|
35
35
|
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
|
+
"qwen-smoke": "tsx scripts/qwen-smoke.mts",
|
|
36
37
|
"render-demo": "tsx scripts/render-demo.mts",
|
|
37
38
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
38
39
|
"postinstall": "node scripts/postinstall.mjs",
|
|
@@ -56,7 +57,11 @@
|
|
|
56
57
|
"browser-automation",
|
|
57
58
|
"llm",
|
|
58
59
|
"no-api-key",
|
|
59
|
-
"automation"
|
|
60
|
+
"automation",
|
|
61
|
+
"qwen",
|
|
62
|
+
"qwen-chat",
|
|
63
|
+
"multi-provider",
|
|
64
|
+
"provider"
|
|
60
65
|
],
|
|
61
66
|
"license": "MIT",
|
|
62
67
|
"engines": {
|
|
@@ -30,7 +30,11 @@ const FLOORS = {
|
|
|
30
30
|
'undo.ts': 85,
|
|
31
31
|
'checkpoint.ts': 80,
|
|
32
32
|
'permissions.ts': 85,
|
|
33
|
-
|
|
33
|
+
// changelog.ts is mostly comments + type/interface declarations (lines
|
|
34
|
+
// 1-30) that carry no runtime statements, so its LINE % caps well below the
|
|
35
|
+
// other modules even with every executable branch tested. The floor reflects
|
|
36
|
+
// that ceiling (a real drop below it still fails).
|
|
37
|
+
'changelog.ts': 78,
|
|
34
38
|
'attach-refs.ts': 85,
|
|
35
39
|
// C3: the pure /config subcommand dispatcher — silent routing regressions
|
|
36
40
|
// (a wrong subcommand branch) are invisible without tests.
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Qwen (chat.qwen.ai) self-smoke: a REAL isolated QwenBrowser instance against
|
|
2
|
+
// the live site, to catch integration regressions (login, send, answer) that
|
|
3
|
+
// unit tests cannot.
|
|
4
|
+
//
|
|
5
|
+
// Isolation: HOME is pointed at a throwaway dir BEFORE importing any project
|
|
6
|
+
// module (they compute ~/.zames paths at load). The real Qwen profile
|
|
7
|
+
// (~/.zames/qwen-profile) is COPIED read-only into the temp HOME so the
|
|
8
|
+
// browser starts already signed in; the live profile is NEVER opened. The
|
|
9
|
+
// Playwright browser cache is symlinked read-only. Nothing in the operator's
|
|
10
|
+
// ~/.zames is written.
|
|
11
|
+
//
|
|
12
|
+
// Usage: npx tsx scripts/qwen-smoke.mts [--headed] [--no-reuse-profile]
|
|
13
|
+
// Exit code 0 = all scenarios passed; 2 = login failed (could not start).
|
|
14
|
+
|
|
15
|
+
import fs from 'fs'
|
|
16
|
+
import path from 'path'
|
|
17
|
+
import os from 'os'
|
|
18
|
+
|
|
19
|
+
const HEADED = process.argv.includes('--headed')
|
|
20
|
+
const REUSE_PROFILE = !process.argv.includes('--no-reuse-profile')
|
|
21
|
+
const REAL_HOME = os.homedir()
|
|
22
|
+
const PROFILE_SRC = path.join(REAL_HOME, '.zames', 'qwen-profile')
|
|
23
|
+
|
|
24
|
+
if (!process.env.ZAMES_QWEN_SMOKE_HOME) {
|
|
25
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'zames-qwen-smoke-'))
|
|
26
|
+
process.env.HOME = tmp
|
|
27
|
+
process.env.ZAMES_QWEN_SMOKE_HOME = tmp
|
|
28
|
+
fs.mkdirSync(path.join(tmp, '.zames'), { recursive: true })
|
|
29
|
+
fs.mkdirSync(path.join(tmp, '.cache'), { recursive: true })
|
|
30
|
+
const realCfg = path.join(REAL_HOME, '.zames', 'config.json')
|
|
31
|
+
if (fs.existsSync(realCfg)) {
|
|
32
|
+
fs.copyFileSync(realCfg, path.join(tmp, '.zames', 'config.json'))
|
|
33
|
+
}
|
|
34
|
+
const realPw = path.join(REAL_HOME, '.cache', 'ms-playwright')
|
|
35
|
+
if (fs.existsSync(realPw)) {
|
|
36
|
+
fs.symlinkSync(realPw, path.join(tmp, '.cache', 'ms-playwright'))
|
|
37
|
+
}
|
|
38
|
+
if (REUSE_PROFILE && fs.existsSync(PROFILE_SRC)) {
|
|
39
|
+
fs.cpSync(PROFILE_SRC, path.join(tmp, '.zames', 'qwen-profile'), {
|
|
40
|
+
recursive: true,
|
|
41
|
+
filter: (src) => !/^Singleton/.test(path.basename(src)),
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const SMOKE_HOME = process.env.ZAMES_QWEN_SMOKE_HOME as string
|
|
46
|
+
|
|
47
|
+
// --- project modules (dynamic: they read HOME at import) --------------------
|
|
48
|
+
const { QwenBrowser } = await import('../src/qwen-browser.ts')
|
|
49
|
+
const { loadConfig } = await import('../src/config.ts')
|
|
50
|
+
|
|
51
|
+
const cfg = loadConfig()
|
|
52
|
+
|
|
53
|
+
interface SmokeResult {
|
|
54
|
+
name: string
|
|
55
|
+
ok: boolean
|
|
56
|
+
note?: string
|
|
57
|
+
}
|
|
58
|
+
const results: SmokeResult[] = []
|
|
59
|
+
let loginFailed = false
|
|
60
|
+
const record = (name: string, ok: boolean, note?: string): void => {
|
|
61
|
+
results.push({ name, ok, note })
|
|
62
|
+
console.log((ok ? 'PASS ' : 'FAIL ') + name + (note ? ' — ' + note : ''))
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const browser = new QwenBrowser({
|
|
66
|
+
headless: !HEADED,
|
|
67
|
+
debug: process.env.ZAMES_QWEN_SMOKE_DEBUG === '1',
|
|
68
|
+
channel: cfg.browserChannel,
|
|
69
|
+
auth: cfg.browser.auth,
|
|
70
|
+
locale: 'ru',
|
|
71
|
+
minSendIntervalMs: 0,
|
|
72
|
+
answerTimeoutMs: 180000,
|
|
73
|
+
webSearch: false,
|
|
74
|
+
deepThinking: false,
|
|
75
|
+
})
|
|
76
|
+
browser.onNotice = (t: string) => console.log(' [notice] ' + t)
|
|
77
|
+
|
|
78
|
+
async function main() {
|
|
79
|
+
console.log(
|
|
80
|
+
'qwen-smoke: HOME=' +
|
|
81
|
+
SMOKE_HOME +
|
|
82
|
+
' headed=' +
|
|
83
|
+
HEADED +
|
|
84
|
+
' reuse-profile=' +
|
|
85
|
+
REUSE_PROFILE,
|
|
86
|
+
)
|
|
87
|
+
await browser.launch()
|
|
88
|
+
|
|
89
|
+
// Bound the login wait so a cold/expired session fails fast with a clear
|
|
90
|
+
// hint instead of hanging until the caller's tool timeout.
|
|
91
|
+
const loginPromise = browser.waitForLogin()
|
|
92
|
+
const timedOut = await Promise.race([
|
|
93
|
+
loginPromise.then(() => false),
|
|
94
|
+
new Promise<boolean>((r) => setTimeout(() => r(true), 60000)),
|
|
95
|
+
])
|
|
96
|
+
if (timedOut) {
|
|
97
|
+
loginFailed = true
|
|
98
|
+
record('login', false, 'timed out after 60s')
|
|
99
|
+
} else {
|
|
100
|
+
const logged = await browser.isLoggedIn().catch(() => false)
|
|
101
|
+
record('login', logged, logged ? '' : 'not signed in (guest)')
|
|
102
|
+
if (!logged) loginFailed = true
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!loginFailed) {
|
|
106
|
+
// 1) new chat + a SIMPLE ask that needs no tools: the model must answer.
|
|
107
|
+
try {
|
|
108
|
+
await browser.newChat()
|
|
109
|
+
const id = await browser.getCurrentChatId()
|
|
110
|
+
record('newChat', true, 'id=' + (id || '(none)'))
|
|
111
|
+
} catch (e) {
|
|
112
|
+
record('newChat', false, (e as Error).message)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const ans = await browser.ask('Reply with exactly: PONG', {
|
|
117
|
+
agent: true,
|
|
118
|
+
})
|
|
119
|
+
const ok = /PONG/i.test(ans)
|
|
120
|
+
record('ask:simple', ok, ok ? '' : 'got: ' + ans.slice(0, 80))
|
|
121
|
+
} catch (e) {
|
|
122
|
+
record('ask:simple', false, (e as Error).message)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 2) a second send right after the first: guards the send-verify path (the
|
|
126
|
+
// "text entered but send button not pressed" regression).
|
|
127
|
+
try {
|
|
128
|
+
const ans = await browser.ask('Reply with exactly: PONG2', {
|
|
129
|
+
agent: true,
|
|
130
|
+
})
|
|
131
|
+
const ok = /PONG2/i.test(ans)
|
|
132
|
+
record('ask:second-send', ok, ok ? '' : 'got: ' + ans.slice(0, 80))
|
|
133
|
+
} catch (e) {
|
|
134
|
+
record('ask:second-send', false, (e as Error).message)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3) model catalogue + token usage (best-effort, non-fatal).
|
|
138
|
+
try {
|
|
139
|
+
const models = await browser.listModels()
|
|
140
|
+
record('listModels', models.length > 0, 'n=' + models.length)
|
|
141
|
+
} catch (e) {
|
|
142
|
+
record('listModels', false, (e as Error).message)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const usage = browser.getLastTokenUsage()
|
|
146
|
+
record('tokenUsage', usage !== null, 'usage=' + String(usage))
|
|
147
|
+
|
|
148
|
+
// 4) tool-call emission: the agent-loop depends on the model returning a
|
|
149
|
+
// parseable JSON tool call. Ask for one explicitly and check the PARSER
|
|
150
|
+
// (not the model's prose).
|
|
151
|
+
try {
|
|
152
|
+
const { parseToolCall } = await import('../src/answer-parse.ts')
|
|
153
|
+
const ans = await browser.ask(
|
|
154
|
+
'Call the Read tool for the file package.json. Reply with EXACTLY one ' +
|
|
155
|
+
'JSON tool-call object and nothing else: ' +
|
|
156
|
+
'{"tool":"Read","args":{"path":"..."}}',
|
|
157
|
+
{ agent: true },
|
|
158
|
+
)
|
|
159
|
+
const parsed = parseToolCall(ans)
|
|
160
|
+
const call = Array.isArray(parsed) ? parsed[0] : parsed
|
|
161
|
+
const ok = !!call && call.tool === 'Read'
|
|
162
|
+
record('toolCall', ok, ok ? '' : 'got: ' + ans.slice(0, 100))
|
|
163
|
+
} catch (e) {
|
|
164
|
+
record('toolCall', false, (e as Error).message)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await browser.close().catch(() => {})
|
|
169
|
+
|
|
170
|
+
const failed = results.filter((r) => !r.ok)
|
|
171
|
+
console.log(
|
|
172
|
+
'\nqwen-smoke: ' +
|
|
173
|
+
(results.length - failed.length) +
|
|
174
|
+
'/' +
|
|
175
|
+
results.length +
|
|
176
|
+
' passed',
|
|
177
|
+
)
|
|
178
|
+
if (!process.env.ZAMES_QWEN_SMOKE_KEEP) {
|
|
179
|
+
fs.rmSync(SMOKE_HOME, { recursive: true, force: true })
|
|
180
|
+
} else {
|
|
181
|
+
console.log('kept HOME: ' + SMOKE_HOME)
|
|
182
|
+
}
|
|
183
|
+
process.exit(loginFailed ? 2 : failed.length ? 1 : 0)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
main().catch((e) => {
|
|
187
|
+
console.error('qwen-smoke crashed:', (e as Error).message)
|
|
188
|
+
process.exit(1)
|
|
189
|
+
})
|