zames_pro 2.11.0 → 2.13.0
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/README.md +40 -3
- package/dist/agent-loop.js +20 -0
- package/dist/i18n.js +15 -1
- package/dist/index.js +134 -14
- package/dist/input.js +6 -0
- package/dist/mcp.js +327 -0
- package/dist/system-prompt.js +9 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -83,8 +83,9 @@ workflows from your repository and from `~/.zames`.
|
|
|
83
83
|
|
|
84
84
|
- **AGENTS.md** — project instructions. Put one in the repo root (or in any
|
|
85
85
|
parent folder of the working directory). Global instructions live in
|
|
86
|
-
`~/.zames/AGENTS.md` (and `~/.claude/CLAUDE.md`).
|
|
87
|
-
|
|
86
|
+
`~/.zames/AGENTS.md` (and `~/.claude/CLAUDE.md`). Run `/init` and the agent
|
|
87
|
+
will explore the project and write an AGENTS.md based on the real build/test
|
|
88
|
+
commands and conventions (use `/init --force` to overwrite an existing file).
|
|
88
89
|
- **MEMORY.md** — durable notes that persist between sessions. The agent
|
|
89
90
|
appends useful facts here; you can edit it by hand.
|
|
90
91
|
- **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
|
|
@@ -102,9 +103,45 @@ Skills and custom commands show up in the «/» completion list and in `/help`.
|
|
|
102
103
|
```
|
|
103
104
|
/skills list discovered skills
|
|
104
105
|
/memory show AGENTS.md / MEMORY.md in effect
|
|
105
|
-
/init
|
|
106
|
+
/init [--force] analyze the project and create AGENTS.md
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
## MCP (external tools)
|
|
110
|
+
|
|
111
|
+
zames can use tools from [MCP](https://modelcontextprotocol.io) servers.
|
|
112
|
+
The flagship example is @playwright/mcp: it gives the agent a real browser
|
|
113
|
+
(navigate, click, snapshot, type, ...) on top of the one zames already uses
|
|
114
|
+
for the DeepSeek chat.
|
|
115
|
+
|
|
116
|
+
Drop a config file (same shape as Claude Code / Cursor):
|
|
117
|
+
|
|
118
|
+
- `~/.zames/mcp.json` - global
|
|
119
|
+
- `<project>/.zames/mcp.json` - project-scoped (later files win)
|
|
120
|
+
- `<project>/.mcp.json` - the common MCP name
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"playwright": {
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": ["-y", "@playwright/mcp@latest", "--headless", "--isolated"]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
IMPORTANT: keep the MCP browser isolated. @playwright/mcp defaults to the
|
|
134
|
+
SAME profile directory as zames (~/.zames/profile). If it is launched
|
|
135
|
+
without --isolated (or without its own --user-data-dir), the MCP browser
|
|
136
|
+
and the agent browser fight over one profile and the DeepSeek chat shows
|
|
137
|
+
"Something went wrong when opening your profile". Always pass --isolated
|
|
138
|
+
as in the example above.
|
|
139
|
+
Servers can also be remote ("url": "https://...", "transport": "sse").
|
|
140
|
+
Their tools show up in the agent as `server__tool` (e.g.
|
|
141
|
+
`playwright__browser_navigate`) and are listed with `/mcp` and in `/status`.
|
|
142
|
+
A server that fails to connect is skipped with a warning and never breaks the
|
|
143
|
+
agent.
|
|
144
|
+
|
|
108
145
|
## Configuration
|
|
109
146
|
|
|
110
147
|
Global config: `~/.zames/config.json`
|
package/dist/agent-loop.js
CHANGED
|
@@ -932,6 +932,26 @@ function parseToolCallPermissive(text) {
|
|
|
932
932
|
if (!toolMatch)
|
|
933
933
|
return null;
|
|
934
934
|
const tool = toolMatch[1];
|
|
935
|
+
// The model occasionally puts the argument keys INLINE with "tool" —
|
|
936
|
+
// {"tool": "Bash", "command": "..."} — with no "args" wrapper at all.
|
|
937
|
+
// The strict parser rejects it (no obj.args), and the permissive parser
|
|
938
|
+
// used to bail out too (indexOf('"args"') === -1). Such a call was
|
|
939
|
+
// reported as malformed and re-asked up to MAX_MALFORMED_RETRIES times;
|
|
940
|
+
// after the budget ran out the run stopped with the model's text as the
|
|
941
|
+
// final answer. Flatten the inline keys into args when "args" is absent.
|
|
942
|
+
if (text.indexOf('"args"') === -1) {
|
|
943
|
+
const braceIdx = text.indexOf('{');
|
|
944
|
+
if (braceIdx === -1)
|
|
945
|
+
return null;
|
|
946
|
+
const endBrace = findMatching(text, braceIdx, '{', '}');
|
|
947
|
+
const objText = endBrace === -1 ? text.slice(braceIdx) : text.slice(braceIdx, endBrace + 1);
|
|
948
|
+
const inlineArgs = parseArgsPermissive(objText) || parseArgsGreedy(objText);
|
|
949
|
+
if (!inlineArgs)
|
|
950
|
+
return null;
|
|
951
|
+
delete inlineArgs.tool;
|
|
952
|
+
// The value may be a string ("command": "...") or a number/bool.
|
|
953
|
+
return { tool, args: inlineArgs };
|
|
954
|
+
}
|
|
935
955
|
const argsIdx = text.indexOf('"args"');
|
|
936
956
|
if (argsIdx === -1)
|
|
937
957
|
return null;
|
package/dist/i18n.js
CHANGED
|
@@ -89,11 +89,15 @@ const CATALOG = {
|
|
|
89
89
|
'memory.memory': { ru: 'Память (MEMORY.md):', en: 'Memory (MEMORY.md):' },
|
|
90
90
|
'init.exists': { ru: 'AGENTS.md уже существует: {v}', en: 'AGENTS.md already exists: {v}' },
|
|
91
91
|
'init.created': { ru: 'Создан {v}', en: 'Created {v}' },
|
|
92
|
+
'init.analyzing': { ru: 'Изучаю проект и готовлю AGENTS.md...', en: 'Analyzing the project and preparing AGENTS.md...' },
|
|
93
|
+
'init.done': { ru: 'AGENTS.md создан агентом: {v}', en: 'AGENTS.md created by the agent: {v}' },
|
|
94
|
+
'init.failed': { ru: 'Не удалось создать AGENTS.md автоматически, записан базовый шаблон.', en: 'Could not create AGENTS.md automatically, wrote a basic template.' },
|
|
95
|
+
'init.overwrite': { ru: 'AGENTS.md уже существует: {v} (используйте /init --force для перезаписи)', en: 'AGENTS.md already exists: {v} (use /init --force to overwrite)' },
|
|
92
96
|
'help.cmd.skill': { ru: 'навык (SKILL.md)', en: 'skill (SKILL.md)' },
|
|
93
97
|
'help.cmd.custom': { ru: 'пользовательская команда', en: 'custom command' },
|
|
94
98
|
'help.cmd.skills': { ru: '/skills список найденных навыков', en: '/skills list discovered skills' },
|
|
95
99
|
'help.cmd.memory': { ru: '/memory показать MEMORY.md и AGENTS.md', en: '/memory show MEMORY.md and AGENTS.md' },
|
|
96
|
-
'help.cmd.init': { ru: '/init
|
|
100
|
+
'help.cmd.init': { ru: '/init [--force] изучить проект и создать AGENTS.md', en: '/init [--force] analyze the project and create AGENTS.md' },
|
|
97
101
|
'help.skills': { ru: 'Навыки и свои команды:', en: 'Skills and custom commands:' },
|
|
98
102
|
'help.self_review': { ru: 'Самообзор (отладка агента):', en: 'Self-review (agent debugging):' },
|
|
99
103
|
'help.self.review': { ru: '/self-review [фокус] снять снапшот src/ и запустить ревью', en: '/self-review [focus] snapshot src/ and run review' },
|
|
@@ -242,6 +246,16 @@ const CATALOG = {
|
|
|
242
246
|
'cd.failed': { ru: 'Не удалось перейти: {v}', en: 'Failed to change directory: {v}' },
|
|
243
247
|
'calibrate': { ru: '\n🔧 Режим калибровки селекторов\n', en: '\n🔧 Selector calibration mode\n' },
|
|
244
248
|
'reload.auto_partial': { ru: '⚠ авто-reload: часть модулей не загрузилась, работаю на прежней версии:', en: '⚠ auto-reload: some modules failed, using previous version:' },
|
|
249
|
+
// ---------- MCP ----------
|
|
250
|
+
'help.cmd.mcp': { ru: '/mcp показать MCP-серверы и их инструменты', en: '/mcp show MCP servers and their tools' },
|
|
251
|
+
'mcp.loaded': { ru: 'MCP: подключено инструментов {n} ({servers})', en: 'MCP: {n} tool(s) from {servers}' },
|
|
252
|
+
'mcp.server_error': { ru: 'MCP-сервер {name} недоступен: {error}', en: 'MCP server {name} unavailable: {error}' },
|
|
253
|
+
'mcp.load_failed': { ru: 'Не удалось загрузить MCP: {v}', en: 'Failed to load MCP: {v}' },
|
|
254
|
+
'mcp.none': { ru: 'MCP-серверы не настроены.', en: 'No MCP servers configured.' },
|
|
255
|
+
'mcp.hint': { ru: 'Добавь ~/.zames/mcp.json или <project>/.zames/mcp.json.', en: 'Add ~/.zames/mcp.json or <project>/.zames/mcp.json.' },
|
|
256
|
+
'mcp.title': { ru: 'MCP-серверы (инструментов: {n}):', en: 'MCP servers ({n} tools):' },
|
|
257
|
+
'mcp.status_error': { ru: '(ошибка: {v})', en: '(error: {v})' },
|
|
258
|
+
'status.mcp': { ru: 'MCP-инструменты: {v}', en: 'MCP tools: {v}' },
|
|
245
259
|
// ---------- config menu ----------
|
|
246
260
|
'cfg.group.ui': { ru: 'Интерфейс', en: 'Interface' },
|
|
247
261
|
'cfg.group.agent': { ru: 'Агент', en: 'Agent' },
|
package/dist/index.js
CHANGED
|
@@ -91,6 +91,7 @@ function loadLastChat(workdir = '') {
|
|
|
91
91
|
// so we pick the extension ourselves — from the actual file of the current module.
|
|
92
92
|
const SRC_EXT = /[.]ts$/.test(new URL(import.meta.url).pathname) ? '.ts' : '.js';
|
|
93
93
|
const RELOADABLE = [
|
|
94
|
+
'mcp',
|
|
94
95
|
'tools',
|
|
95
96
|
'agent-loop',
|
|
96
97
|
'system-prompt',
|
|
@@ -114,6 +115,7 @@ const mod = {
|
|
|
114
115
|
selfList,
|
|
115
116
|
closeWeb,
|
|
116
117
|
createSpinner,
|
|
118
|
+
createMcpPool: null,
|
|
117
119
|
};
|
|
118
120
|
async function reloadModules() {
|
|
119
121
|
const stamp = Date.now();
|
|
@@ -150,6 +152,8 @@ async function reloadModules() {
|
|
|
150
152
|
mod.closeWeb = pick('web', 'closeWeb');
|
|
151
153
|
if (pick('spinner', 'createSpinner'))
|
|
152
154
|
mod.createSpinner = pick('spinner', 'createSpinner');
|
|
155
|
+
if (pick('mcp', 'createMcpPool'))
|
|
156
|
+
mod.createMcpPool = pick('mcp', 'createMcpPool');
|
|
153
157
|
return { count: loaded.size, errors };
|
|
154
158
|
}
|
|
155
159
|
// Initial load so that mod.buildSystemPrompt and the rest are populated.
|
|
@@ -265,6 +269,7 @@ const SLASH_COMMANDS = [
|
|
|
265
269
|
{ name: '/skills', key: 'help.cmd.skills' },
|
|
266
270
|
{ name: '/memory', key: 'help.cmd.memory' },
|
|
267
271
|
{ name: '/init', key: 'help.cmd.init' },
|
|
272
|
+
{ name: '/mcp', key: 'help.cmd.mcp' },
|
|
268
273
|
{ name: '/debug-dom', key: 'help.cmd.debug_dom' },
|
|
269
274
|
{ name: '/self-review', key: 'help.self.review' },
|
|
270
275
|
{ name: '/self-fix', key: 'help.self.fix' },
|
|
@@ -901,6 +906,23 @@ async function main() {
|
|
|
901
906
|
console.log(theme.system(t('msg.transcript', { v: transcript.file })));
|
|
902
907
|
}
|
|
903
908
|
const undo = new UndoStore(config.undo);
|
|
909
|
+
// MCP servers: optional external tool providers (e.g. @playwright/mcp).
|
|
910
|
+
let mcpPool = null;
|
|
911
|
+
try {
|
|
912
|
+
mcpPool = await mod.createMcpPool({ workdir: currentWorkdir });
|
|
913
|
+
const st = mcpPool.status();
|
|
914
|
+
if (st.toolCount > 0) {
|
|
915
|
+
const names = st.servers.filter((x) => !x.error).map((x) => x.name).join(', ');
|
|
916
|
+
console.log(theme.system(t('mcp.loaded', { n: String(st.toolCount), servers: names })));
|
|
917
|
+
}
|
|
918
|
+
for (const srv of st.servers) {
|
|
919
|
+
if (srv.error)
|
|
920
|
+
console.error(theme.warn(t('mcp.server_error', { name: srv.name, error: srv.error })));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
catch (e) {
|
|
924
|
+
console.error(theme.warn(t('mcp.load_failed', { v: e.message })));
|
|
925
|
+
}
|
|
904
926
|
const browser = new DeepSeekBrowser({
|
|
905
927
|
headless,
|
|
906
928
|
debug,
|
|
@@ -926,6 +948,8 @@ async function main() {
|
|
|
926
948
|
// One-shot mode
|
|
927
949
|
if (task) {
|
|
928
950
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
951
|
+
if (mcpPool)
|
|
952
|
+
tools.push(...mcpPool.tools);
|
|
929
953
|
let freshChat = true;
|
|
930
954
|
let sendSystemPrompt = true;
|
|
931
955
|
// By default we start a new chat. To continue a previous session —
|
|
@@ -962,6 +986,8 @@ async function main() {
|
|
|
962
986
|
});
|
|
963
987
|
await browser.close();
|
|
964
988
|
await mod.closeWeb().catch(() => { });
|
|
989
|
+
if (mcpPool)
|
|
990
|
+
await mcpPool.close().catch(() => { });
|
|
965
991
|
transcript.close();
|
|
966
992
|
return;
|
|
967
993
|
}
|
|
@@ -990,6 +1016,8 @@ async function main() {
|
|
|
990
1016
|
// Close the lazy headless browser from web.js, otherwise it would stay
|
|
991
1017
|
// as a separate process after the agent exits.
|
|
992
1018
|
await mod.closeWeb().catch(() => { });
|
|
1019
|
+
if (mcpPool)
|
|
1020
|
+
await mcpPool.close().catch(() => { });
|
|
993
1021
|
transcript.close();
|
|
994
1022
|
console.log(theme.system(String.fromCharCode(10) + t('msg.bye')));
|
|
995
1023
|
process.exit(0);
|
|
@@ -1752,6 +1780,24 @@ async function main() {
|
|
|
1752
1780
|
console.log(theme.system(currentWorkdir));
|
|
1753
1781
|
continue;
|
|
1754
1782
|
}
|
|
1783
|
+
if (lower === '/mcp') {
|
|
1784
|
+
if (!mcpPool) {
|
|
1785
|
+
console.log(theme.dim(t('mcp.none')));
|
|
1786
|
+
console.log(theme.dim(t('mcp.hint')));
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
const st = mcpPool.status();
|
|
1790
|
+
console.log(theme.system(t('mcp.title', { n: String(st.toolCount) })));
|
|
1791
|
+
for (const srv of st.servers) {
|
|
1792
|
+
if (srv.error) {
|
|
1793
|
+
console.log(' ' + theme.user(srv.name) + ' ' + theme.dim(t('mcp.status_error', { v: srv.error })));
|
|
1794
|
+
}
|
|
1795
|
+
else {
|
|
1796
|
+
console.log(' ' + theme.user(srv.name) + ' ' + theme.dim('(' + String(srv.tools.length) + ')'));
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
continue;
|
|
1800
|
+
}
|
|
1755
1801
|
if (lower === '/skills') {
|
|
1756
1802
|
const { loadSkills } = await import('./context.js');
|
|
1757
1803
|
const skills = await loadSkills(currentWorkdir);
|
|
@@ -1786,24 +1832,91 @@ async function main() {
|
|
|
1786
1832
|
show(t('memory.memory'), ctx.memory);
|
|
1787
1833
|
continue;
|
|
1788
1834
|
}
|
|
1789
|
-
if (lower === '/init') {
|
|
1835
|
+
if (lower === '/init' || lower.startsWith('/init ')) {
|
|
1836
|
+
const force = lower.includes('--force');
|
|
1790
1837
|
const target = path.join(currentWorkdir, 'AGENTS.md');
|
|
1791
1838
|
const exists = await fs.stat(target).catch(() => null);
|
|
1792
|
-
if (exists) {
|
|
1793
|
-
console.log(theme.warn(t('init.
|
|
1839
|
+
if (exists && !force) {
|
|
1840
|
+
console.log(theme.warn(t('init.overwrite', { v: target })));
|
|
1794
1841
|
continue;
|
|
1795
1842
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
'
|
|
1801
|
-
'
|
|
1802
|
-
'
|
|
1803
|
-
'
|
|
1804
|
-
'
|
|
1805
|
-
|
|
1806
|
-
|
|
1843
|
+
// Like Codex: let the agent explore the project and write AGENTS.md via
|
|
1844
|
+
// the Write tool, so the file reflects the real build/test commands and
|
|
1845
|
+
// conventions instead of a static template.
|
|
1846
|
+
const initTask = 'The user ran /init. Analyze this repository and create an AGENTS.md ' +
|
|
1847
|
+
'file at the project root that onboards future coding agents. Explore ' +
|
|
1848
|
+
'the project first (read the README, package.json and other manifests, ' +
|
|
1849
|
+
'configs, CI, and a sample of source files). Then write AGENTS.md with: ' +
|
|
1850
|
+
'an overview of what the project is; build/typecheck/test/lint commands ' +
|
|
1851
|
+
'(use the real scripts you find); code style and conventions; the repo ' +
|
|
1852
|
+
'layout; and any gotchas or rules an agent must follow. Keep it concise ' +
|
|
1853
|
+
'and factual, based only on what you find. Write the file with the Write ' +
|
|
1854
|
+
'tool, then reply via respond with a one-line summary.';
|
|
1855
|
+
const initTools = mod.createTools(currentWorkdir, { undo });
|
|
1856
|
+
if (editor)
|
|
1857
|
+
editor.busy = true;
|
|
1858
|
+
console.log(theme.system(t('init.analyzing')));
|
|
1859
|
+
try {
|
|
1860
|
+
await mod.runAgentLoop({
|
|
1861
|
+
browser,
|
|
1862
|
+
tools: initTools,
|
|
1863
|
+
task: initTask,
|
|
1864
|
+
workdir: currentWorkdir,
|
|
1865
|
+
maxIterations: maxIter,
|
|
1866
|
+
freshChat: freshChatNext,
|
|
1867
|
+
sendSystemPrompt: sendSystemPromptNext,
|
|
1868
|
+
transcript,
|
|
1869
|
+
onThinking: () => { },
|
|
1870
|
+
onToolCall: (name, toolArgs) => {
|
|
1871
|
+
if (editor)
|
|
1872
|
+
editor.toolCall(name, toolArgs);
|
|
1873
|
+
},
|
|
1874
|
+
onToolResult: (r) => {
|
|
1875
|
+
if (editor)
|
|
1876
|
+
editor.toolResult(r);
|
|
1877
|
+
},
|
|
1878
|
+
onAssistantMessage: (m) => {
|
|
1879
|
+
if (editor)
|
|
1880
|
+
editor.assistant(m);
|
|
1881
|
+
},
|
|
1882
|
+
onWarning: (m) => {
|
|
1883
|
+
if (editor)
|
|
1884
|
+
editor.warning(m);
|
|
1885
|
+
},
|
|
1886
|
+
locale: currentLocale,
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
catch (e) {
|
|
1890
|
+
console.error(theme.error(t('init.failed')), e.message);
|
|
1891
|
+
}
|
|
1892
|
+
finally {
|
|
1893
|
+
if (editor)
|
|
1894
|
+
editor.busy = false;
|
|
1895
|
+
}
|
|
1896
|
+
freshChatNext = false;
|
|
1897
|
+
sendSystemPromptNext = false;
|
|
1898
|
+
if (!currentChatId) {
|
|
1899
|
+
currentChatId = await browser.getCurrentChatId();
|
|
1900
|
+
}
|
|
1901
|
+
saveLastChat(currentChatId, currentWorkdir);
|
|
1902
|
+
const created = await fs.stat(target).catch(() => null);
|
|
1903
|
+
if (created) {
|
|
1904
|
+
console.log(theme.assistant(t('init.done', { v: target })));
|
|
1905
|
+
}
|
|
1906
|
+
else {
|
|
1907
|
+
const stub = '# AGENTS.md\n\n' +
|
|
1908
|
+
'Project instructions for the coding agent. Describe the build/test commands, ' +
|
|
1909
|
+
'conventions, and any rules the agent must follow in this repository.\n\n' +
|
|
1910
|
+
'## Commands\n\n' +
|
|
1911
|
+
'- build: `...`\n' +
|
|
1912
|
+
'- test: `...`\n' +
|
|
1913
|
+
'- lint: `...`\n\n' +
|
|
1914
|
+
'## Conventions\n\n' +
|
|
1915
|
+
'- ...\n';
|
|
1916
|
+
await fs.writeFile(target, stub, 'utf-8');
|
|
1917
|
+
console.log(theme.warn(t('init.failed')));
|
|
1918
|
+
console.log(theme.assistant(t('init.created', { v: target })));
|
|
1919
|
+
}
|
|
1807
1920
|
continue;
|
|
1808
1921
|
}
|
|
1809
1922
|
if (lower === '/reload') {
|
|
@@ -1859,6 +1972,9 @@ async function main() {
|
|
|
1859
1972
|
v: transcript.file || t('common.off'),
|
|
1860
1973
|
})));
|
|
1861
1974
|
console.log(theme.system(t('status.locale', { v: localeDisplayName(currentLocale) })));
|
|
1975
|
+
console.log(theme.system(t('status.mcp', {
|
|
1976
|
+
v: mcpPool ? String(mcpPool.status().toolCount) : t('common.none'),
|
|
1977
|
+
})));
|
|
1862
1978
|
continue;
|
|
1863
1979
|
}
|
|
1864
1980
|
if (lower === '/config' || lower.startsWith('/config ')) {
|
|
@@ -1965,6 +2081,8 @@ async function main() {
|
|
|
1965
2081
|
await autoReload();
|
|
1966
2082
|
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
1967
2083
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
2084
|
+
if (mcpPool)
|
|
2085
|
+
tools.push(...mcpPool.tools);
|
|
1968
2086
|
if (editor)
|
|
1969
2087
|
editor.busy = true;
|
|
1970
2088
|
try {
|
|
@@ -2000,6 +2118,8 @@ async function main() {
|
|
|
2000
2118
|
editor.dispose();
|
|
2001
2119
|
await browser.close().catch(() => { });
|
|
2002
2120
|
await mod.closeWeb().catch(() => { });
|
|
2121
|
+
if (mcpPool)
|
|
2122
|
+
await mcpPool.close().catch(() => { });
|
|
2003
2123
|
transcript.close();
|
|
2004
2124
|
}
|
|
2005
2125
|
main().catch((e) => {
|
package/dist/input.js
CHANGED
|
@@ -351,6 +351,12 @@ export class LineEditor {
|
|
|
351
351
|
}
|
|
352
352
|
unlock() {
|
|
353
353
|
this.locked = false;
|
|
354
|
+
// A lock may have set the status hint ("operation in progress, input is
|
|
355
|
+
// temporarily locked"). If nothing else replaced it (no spinner, no
|
|
356
|
+
// pending text), clear it here — otherwise the hint stays on screen
|
|
357
|
+
// forever after a fast operation like /new.
|
|
358
|
+
if (this.statusText)
|
|
359
|
+
this.setStatus('');
|
|
354
360
|
}
|
|
355
361
|
setPrompt(str) {
|
|
356
362
|
this.promptStr = str;
|
package/dist/mcp.js
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/** Names of the servers found in a config, before any connection. */
|
|
2
|
+
export async function listConfiguredServers(workdir) {
|
|
3
|
+
const files = await discoverConfigFiles(workdir);
|
|
4
|
+
const config = await loadConfigFiles(files);
|
|
5
|
+
const servers = config.mcpServers || {};
|
|
6
|
+
return Object.keys(servers).filter((n) => !servers[n].disabled);
|
|
7
|
+
}
|
|
8
|
+
// @playwright/mcp defaults its --user-data-dir to the SAME profile zames uses
|
|
9
|
+
// (~/.zames/profile). Two chromium instances on one profile break each other:
|
|
10
|
+
// the chat page shows "Something went wrong when opening your profile" and
|
|
11
|
+
// the session is lost. So when a stdio server looks like @playwright/mcp we
|
|
12
|
+
// auto-add --isolated (in-memory profile) unless the user already chose a
|
|
13
|
+
// profile (--isolated / --user-data-dir / --extension / --cdp-endpoint).
|
|
14
|
+
export function hardenPlaywrightArgs(command, args) {
|
|
15
|
+
const hay = (String(command || '') + ' ' + args.join(' ')).toLowerCase();
|
|
16
|
+
if (!hay.includes('playwright') || !hay.includes('mcp'))
|
|
17
|
+
return args;
|
|
18
|
+
if (args.some((a) => a === '--isolated' || a.startsWith('--user-data-dir') || a === '--extension' || a.startsWith('--cdp-endpoint'))) {
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
return [...args, '--isolated'];
|
|
22
|
+
}
|
|
23
|
+
/** Parse one config object into a validated map (best-effort). */
|
|
24
|
+
export function normalizeServerConfig(raw) {
|
|
25
|
+
if (!raw || typeof raw !== 'object')
|
|
26
|
+
return null;
|
|
27
|
+
const r = raw;
|
|
28
|
+
const out = {};
|
|
29
|
+
if (typeof r.command === 'string')
|
|
30
|
+
out.command = r.command;
|
|
31
|
+
if (Array.isArray(r.args))
|
|
32
|
+
out.args = r.args.map((a) => String(a));
|
|
33
|
+
if (r.env && typeof r.env === 'object') {
|
|
34
|
+
out.env = {};
|
|
35
|
+
for (const [k, v] of Object.entries(r.env)) {
|
|
36
|
+
out.env[k] = String(v);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (typeof r.cwd === 'string')
|
|
40
|
+
out.cwd = r.cwd;
|
|
41
|
+
if (typeof r.url === 'string')
|
|
42
|
+
out.url = r.url;
|
|
43
|
+
if (r.transport === 'sse' || r.transport === 'streamable-http') {
|
|
44
|
+
out.transport = r.transport;
|
|
45
|
+
}
|
|
46
|
+
if (r.headers && typeof r.headers === 'object') {
|
|
47
|
+
out.headers = {};
|
|
48
|
+
for (const [k, v] of Object.entries(r.headers)) {
|
|
49
|
+
out.headers[k] = String(v);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (r.disabled === true)
|
|
53
|
+
out.disabled = true;
|
|
54
|
+
if (!out.command && !out.url)
|
|
55
|
+
return null;
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
/** Candidate config paths, ordered from global to project-local. */
|
|
59
|
+
export async function discoverConfigFiles(workdir) {
|
|
60
|
+
const path = await import('path');
|
|
61
|
+
const os = await import('os');
|
|
62
|
+
const fsMod = await import('fs/promises');
|
|
63
|
+
const candidates = [
|
|
64
|
+
path.join(os.homedir(), '.zames', 'mcp.json'),
|
|
65
|
+
path.join(workdir, '.zames', 'mcp.json'),
|
|
66
|
+
path.join(workdir, '.mcp.json'),
|
|
67
|
+
];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const c of candidates) {
|
|
70
|
+
if (await fsMod.stat(c).catch(() => null))
|
|
71
|
+
out.push(c);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
/** Read and merge every config file. Later files override earlier ones. */
|
|
76
|
+
export async function loadConfigFiles(files) {
|
|
77
|
+
const merged = {};
|
|
78
|
+
const fsMod = await import('fs/promises');
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
try {
|
|
81
|
+
const raw = await fsMod.readFile(f, 'utf-8');
|
|
82
|
+
const json = JSON.parse(raw);
|
|
83
|
+
const servers = json.mcpServers;
|
|
84
|
+
if (!servers || typeof servers !== 'object')
|
|
85
|
+
continue;
|
|
86
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
87
|
+
const normalized = normalizeServerConfig(cfg);
|
|
88
|
+
if (normalized)
|
|
89
|
+
merged[name] = normalized;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Best-effort: a broken MCP config must not break the agent.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { mcpServers: merged };
|
|
97
|
+
}
|
|
98
|
+
/* ---------- json-schema -> agent parameters ---------- */
|
|
99
|
+
// MCP tools describe their arguments with a JSON Schema. zames own ToolDef
|
|
100
|
+
// uses a flat map (name -> 'type?'), which is what the system-prompt shows
|
|
101
|
+
// to the model. We convert the schema into that flat form so MCP tools look
|
|
102
|
+
// like built-in ones and the model already knows the convention.
|
|
103
|
+
//
|
|
104
|
+
// We also keep the full schema in the tool description, so the model sees
|
|
105
|
+
// descriptions/enums/required fields that the flat map cannot convey.
|
|
106
|
+
export function jsonSchemaToParams(schema) {
|
|
107
|
+
const out = {};
|
|
108
|
+
if (!schema || typeof schema !== 'object')
|
|
109
|
+
return out;
|
|
110
|
+
const s = schema;
|
|
111
|
+
if (!s.properties || typeof s.properties !== 'object')
|
|
112
|
+
return out;
|
|
113
|
+
const required = new Set(Array.isArray(s.required) ? s.required : []);
|
|
114
|
+
for (const [key, raw] of Object.entries(s.properties)) {
|
|
115
|
+
const prop = (raw || {});
|
|
116
|
+
let type = typeof prop.type === 'string' ? prop.type : '';
|
|
117
|
+
if (Array.isArray(prop.type) || prop.enum)
|
|
118
|
+
type = 'value';
|
|
119
|
+
if (!type)
|
|
120
|
+
type = 'value';
|
|
121
|
+
out[key] = type + (required.has(key) ? '' : '?');
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
// Append the raw JSON Schema to the description so the model sees field
|
|
126
|
+
// descriptions and enums. The flat map alone cannot convey them, and MCP
|
|
127
|
+
// tools (browser_snapshot, browser_click, ...) rely on them heavily.
|
|
128
|
+
export function describeParamsExternally(description, schema) {
|
|
129
|
+
const type = schema?.type;
|
|
130
|
+
if (!schema || type !== 'object')
|
|
131
|
+
return description;
|
|
132
|
+
try {
|
|
133
|
+
const json = JSON.stringify(schema);
|
|
134
|
+
if (json.length > 4000)
|
|
135
|
+
return description;
|
|
136
|
+
return description + '\nJSON Schema: ' + json;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return description;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/* ---------- result rendering ---------- */
|
|
143
|
+
// MCP tool results are a list of content blocks (text, image, resource, ...)
|
|
144
|
+
// and an isError flag. The agent needs a single string for the tool result
|
|
145
|
+
// message. Images are summarized (the model cannot see them through
|
|
146
|
+
// DeepSeek web anyway), text is joined.
|
|
147
|
+
export function renderMcpResult(result) {
|
|
148
|
+
const r = (result || {});
|
|
149
|
+
if (r.toolResult !== undefined && !r.content) {
|
|
150
|
+
return formatResultString(r.toolResult, r.isError);
|
|
151
|
+
}
|
|
152
|
+
const parts = [];
|
|
153
|
+
for (const block of r.content || []) {
|
|
154
|
+
if (!block || typeof block !== 'object') {
|
|
155
|
+
parts.push(String(block ?? ''));
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const b = block;
|
|
159
|
+
const type = String(b.type ?? '');
|
|
160
|
+
if (type === 'text') {
|
|
161
|
+
parts.push(String(b.text ?? ''));
|
|
162
|
+
}
|
|
163
|
+
else if (type === 'image') {
|
|
164
|
+
parts.push('[image ' + String(b.mimeType ?? 'unknown') + ', written to disk by the MCP server]');
|
|
165
|
+
}
|
|
166
|
+
else if (type === 'resource') {
|
|
167
|
+
const res = (b.resource || {});
|
|
168
|
+
if (typeof res.text === 'string') {
|
|
169
|
+
parts.push(String(res.uri ?? '') + String.fromCharCode(10) + res.text);
|
|
170
|
+
}
|
|
171
|
+
else if (typeof res.blob === 'string') {
|
|
172
|
+
parts.push('[binary resource ' + String(res.uri ?? '') + ', ' + String(res.mimeType ?? '') + ']');
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
parts.push(String(res.uri ?? ''));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (type === 'resource_link') {
|
|
179
|
+
parts.push(String(b.uri ?? b.name ?? ''));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
try {
|
|
183
|
+
parts.push(JSON.stringify(b));
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
parts.push(String(b));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const text = parts.filter((p) => p !== '').join(String.fromCharCode(10) + String.fromCharCode(10)).trim();
|
|
191
|
+
return formatResultString(text || '(MCP tool returned no content)', r.isError);
|
|
192
|
+
}
|
|
193
|
+
function formatResultString(result, isError) {
|
|
194
|
+
const text = typeof result === 'string' ? result : safeJson(result);
|
|
195
|
+
return isError ? 'MCP error: ' + text : text;
|
|
196
|
+
}
|
|
197
|
+
function safeJson(v) {
|
|
198
|
+
try {
|
|
199
|
+
const s = JSON.stringify(v);
|
|
200
|
+
return s === undefined ? String(v) : s;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return String(v);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Namespacing: MCP tool names are prefixed with the server name, so the
|
|
207
|
+
// system-prompt (and the transcript) show where a tool comes from and two
|
|
208
|
+
// servers cannot collide. The prefix uses __ (double underscore) because
|
|
209
|
+
// the model must be able to reproduce it exactly.
|
|
210
|
+
export function qualifyToolName(server, tool) {
|
|
211
|
+
return server + '__' + tool.replace(/[^A-Za-z0-9_]/g, '_');
|
|
212
|
+
}
|
|
213
|
+
// Connect to ONE server and return a ready connection (or an error).
|
|
214
|
+
async function connectServer(name, cfg, debug) {
|
|
215
|
+
try {
|
|
216
|
+
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
217
|
+
const client = new Client({ name: 'zames', version: '1.0.0' }, { capabilities: {} });
|
|
218
|
+
let transport;
|
|
219
|
+
if (cfg.url) {
|
|
220
|
+
if (cfg.transport === 'sse') {
|
|
221
|
+
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
222
|
+
transport = new SSEClientTransport(new URL(cfg.url), {
|
|
223
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
228
|
+
transport = new StreamableHTTPClientTransport(new URL(cfg.url), {
|
|
229
|
+
requestInit: cfg.headers ? { headers: cfg.headers } : undefined,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const { StdioClientTransport } = await import('@modelcontextprotocol/sdk/client/stdio.js');
|
|
235
|
+
transport = new StdioClientTransport({
|
|
236
|
+
command: String(cfg.command),
|
|
237
|
+
args: hardenPlaywrightArgs(String(cfg.command), cfg.args || []),
|
|
238
|
+
env: cfg.env,
|
|
239
|
+
cwd: cfg.cwd,
|
|
240
|
+
stderr: 'pipe',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
await client.connect(transport);
|
|
244
|
+
const listed = await client.listTools();
|
|
245
|
+
const tools = (listed.tools || []).map((t) => {
|
|
246
|
+
const tool = t;
|
|
247
|
+
return {
|
|
248
|
+
name: String(tool.name),
|
|
249
|
+
description: String(tool.description || ''),
|
|
250
|
+
schema: tool.inputSchema,
|
|
251
|
+
};
|
|
252
|
+
});
|
|
253
|
+
debug?.(`MCP: connected to ${name} (${tools.length} tools)`);
|
|
254
|
+
return { name, client, tools };
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
const msg = e.message || String(e);
|
|
258
|
+
debug?.(`MCP: failed to connect to ${name}: ${msg}`);
|
|
259
|
+
return { error: msg };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Build a pool from the MCP configs found for a workdir.
|
|
264
|
+
* Never throws: a broken server is reported in the status and skipped.
|
|
265
|
+
*/
|
|
266
|
+
export async function createMcpPool(opts) {
|
|
267
|
+
const files = await discoverConfigFiles(opts.workdir);
|
|
268
|
+
const config = await loadConfigFiles(files);
|
|
269
|
+
const servers = config.mcpServers || {};
|
|
270
|
+
const names = Object.keys(servers).filter((n) => !servers[n].disabled);
|
|
271
|
+
const infos = [];
|
|
272
|
+
const connections = [];
|
|
273
|
+
const tools = [];
|
|
274
|
+
for (const name of names) {
|
|
275
|
+
const cfg = servers[name];
|
|
276
|
+
const res = await connectServer(name, cfg, opts.debug);
|
|
277
|
+
if ('error' in res) {
|
|
278
|
+
infos.push({ name, tools: [], error: res.error });
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
connections.push(res);
|
|
282
|
+
infos.push({ name, tools: res.tools.map((t) => t.name) });
|
|
283
|
+
for (const tool of res.tools) {
|
|
284
|
+
const qualified = qualifyToolName(name, tool.name);
|
|
285
|
+
const params = jsonSchemaToParams(tool.schema);
|
|
286
|
+
const description = describeParamsExternally((tool.description || tool.name) + ` (MCP server: ${name})`, tool.schema);
|
|
287
|
+
tools.push({
|
|
288
|
+
name: qualified,
|
|
289
|
+
description,
|
|
290
|
+
parameters: params,
|
|
291
|
+
fn: async (args) => {
|
|
292
|
+
try {
|
|
293
|
+
const result = await res.client.callTool({
|
|
294
|
+
name: tool.name,
|
|
295
|
+
arguments: args,
|
|
296
|
+
});
|
|
297
|
+
return renderMcpResult(result);
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
return `MCP error (${name}/${tool.name}): ${e.message}`;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
let closed = false;
|
|
307
|
+
return {
|
|
308
|
+
tools,
|
|
309
|
+
status: () => ({
|
|
310
|
+
servers: infos,
|
|
311
|
+
toolCount: tools.length,
|
|
312
|
+
}),
|
|
313
|
+
close: async () => {
|
|
314
|
+
if (closed)
|
|
315
|
+
return;
|
|
316
|
+
closed = true;
|
|
317
|
+
for (const c of connections) {
|
|
318
|
+
try {
|
|
319
|
+
await c.client.close();
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// ignore
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
package/dist/system-prompt.js
CHANGED
|
@@ -113,6 +113,15 @@ You have access to the following tools:
|
|
|
113
113
|
|
|
114
114
|
${toolDescriptions}
|
|
115
115
|
${gitSection}${attachSection}${contextSection}
|
|
116
|
+
## MCP tools
|
|
117
|
+
|
|
118
|
+
Some tools are provided by external MCP servers and their names look
|
|
119
|
+
like server__tool (for example playwright__browser_navigate). Treat
|
|
120
|
+
them exactly like the built-in tools. A tool name with the __ separator
|
|
121
|
+
and the (MCP server: ...) note in its description tells you which server
|
|
122
|
+
it belongs to. The JSON Schema in the description lists the exact fields
|
|
123
|
+
(including their descriptions and enums) - follow it.
|
|
124
|
+
|
|
116
125
|
## How to use tools
|
|
117
126
|
|
|
118
127
|
To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "Terminal coding agent over chat.deepseek.com via Playwright",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"node": ">=18"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
39
40
|
"chalk": "^5.3.0",
|
|
40
41
|
"cli-highlight": "^2.1.11",
|
|
41
42
|
"markdansi": "^0.3.4",
|