zames_pro 2.10.9 → 2.12.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 CHANGED
@@ -76,6 +76,36 @@ zames --version
76
76
  zames --help
77
77
  ```
78
78
 
79
+ ## Project context, skills and memory
80
+
81
+ Like Codex / Claude Code, zames reads project instructions and reusable
82
+ workflows from your repository and from `~/.zames`.
83
+
84
+ - **AGENTS.md** — project instructions. Put one in the repo root (or in any
85
+ parent folder of the working directory). Global instructions live in
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).
89
+ - **MEMORY.md** — durable notes that persist between sessions. The agent
90
+ appends useful facts here; you can edit it by hand.
91
+ - **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
92
+ `description`, optional `allowed-tools`, `user-invokable`) plus any helper
93
+ files. Discovered under `.zames/skills/`, `.claude/skills/`,
94
+ `.agents/skills/`, `skills/`, and `~/.zames/skills/`. The agent reads the
95
+ body only when a task matches the description. List them with `/skills`;
96
+ invoke one with `/<skill-name>`.
97
+ - **Custom commands** — `.md` files under `.zames/commands/` (or
98
+ `.claude/commands/`). They support `$ARGUMENTS` / `{{args}}` placeholders and
99
+ are invoked with `/<command-name>`.
100
+
101
+ Skills and custom commands show up in the «/» completion list and in `/help`.
102
+
103
+ ```
104
+ /skills list discovered skills
105
+ /memory show AGENTS.md / MEMORY.md in effect
106
+ /init [--force] analyze the project and create AGENTS.md
107
+ ```
108
+
79
109
  ## Configuration
80
110
 
81
111
  Global config: `~/.zames/config.json`
@@ -1,9 +1,10 @@
1
1
  import { buildSystemPrompt } from './system-prompt.js';
2
+ import { loadProjectContext } from './context.js';
2
3
  import { getGitContext, formatGitContext } from './gitTools.js';
3
4
  import { parseXmlToolCalls } from './xml-toolcall.js';
4
5
  import { translate } from './i18n.js';
5
6
  import { normText } from './browser.js';
6
- export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 40, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', }) {
7
+ export async function runAgentLoop({ browser, tools, task, workdir, maxIterations = 200, freshChat = false, sendSystemPrompt = false, transcript = null, attachments = [], onThinking = () => { }, onAssistantThought = () => { }, onToolCall = () => { }, onToolResult = () => { }, onAssistantMessage = () => { }, onChatReady = () => { }, onWarning = () => { }, debugLog = false, locale = 'ru', }) {
7
8
  // UI callbacks must NEVER break the agent loop. A rendering error (a huge
8
9
  // tool result, a broken markdown frame, a closed terminal) used to throw
9
10
  // out of the loop right after a tool call — the session looked "stopped
@@ -59,15 +60,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
59
60
  catch (e) {
60
61
  gitText = `(git context error: ${e.message})`;
61
62
  }
63
+ let context = null;
64
+ try {
65
+ context = await loadProjectContext(workdir);
66
+ }
67
+ catch {
68
+ context = null;
69
+ }
62
70
  const systemPrompt = buildSystemPrompt({
63
71
  workdir,
64
72
  tools,
65
73
  gitContext: gitText,
66
74
  locale,
75
+ context,
67
76
  });
68
77
  transcript?.log('system_prompt', {
69
78
  length: systemPrompt.length,
70
79
  gitContext: gitText,
80
+ agents: context?.agents.map((f) => f.path) ?? [],
81
+ memory: context?.memory.map((f) => f.path) ?? [],
82
+ skills: context?.skills.map((s) => s.name) ?? [],
83
+ commands: context?.commands.map((c) => c.name) ?? [],
71
84
  });
72
85
  safeThinking();
73
86
  // system-prompt is an agent send: throttled (agent: true).
@@ -919,6 +932,26 @@ function parseToolCallPermissive(text) {
919
932
  if (!toolMatch)
920
933
  return null;
921
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
+ }
922
955
  const argsIdx = text.indexOf('"args"');
923
956
  if (argsIdx === -1)
924
957
  return null;
package/dist/config.js CHANGED
@@ -6,7 +6,7 @@ const ZAMES_HOME = path.join(os.homedir(), '.zames');
6
6
  const HOME_CONFIG = path.join(ZAMES_HOME, 'config.json');
7
7
  const PROJECT_CONFIG = path.join(process.cwd(), '.zamesrc.json');
8
8
  export const DEFAULTS = {
9
- maxIterations: 40,
9
+ maxIterations: 200,
10
10
  headless: false,
11
11
  debug: false,
12
12
  browserChannel: null,
@@ -77,7 +77,7 @@ function deepMerge(target, source) {
77
77
  }
78
78
  export const CONFIG_SCHEMA = [
79
79
  { path: 'ui.locale', type: 'enum', values: ['ru', 'en'], labelKey: 'cfg.f.ui_locale', groupKey: 'cfg.group.ui' },
80
- { path: 'maxIterations', type: 'number', min: 1, max: 1000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
80
+ { path: 'maxIterations', type: 'number', min: 1, max: 100000, labelKey: 'cfg.f.maxIterations', groupKey: 'cfg.group.agent' },
81
81
  { path: 'headless', type: 'boolean', labelKey: 'cfg.f.headless', groupKey: 'cfg.group.agent' },
82
82
  { path: 'debug', type: 'boolean', labelKey: 'cfg.f.debug', groupKey: 'cfg.group.agent' },
83
83
  { path: 'hotReload', type: 'boolean', labelKey: 'cfg.f.hotReload', groupKey: 'cfg.group.agent' },
@@ -0,0 +1,270 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import os from 'os';
4
+ const MAX_FILE_BASE = 60000;
5
+ const MAX_TOTAL = 240000;
6
+ const CRLF_RE = /\r\n/g;
7
+ const NL = String.fromCharCode(10);
8
+ function clipIf(name, content, budget) {
9
+ const cleaned = content.replace(CRLF_RE, NL).trim();
10
+ if (cleaned.length <= budget)
11
+ return cleaned;
12
+ const keep = Math.max(0, budget - 120);
13
+ return cleaned.slice(0, keep) + NL + NL + '[... ' + name + ' truncated (' + (cleaned.length - keep) + ' chars omitted) ...]';
14
+ }
15
+ async function readIfFile(p, budget) {
16
+ try {
17
+ const st = await fs.stat(p);
18
+ if (!st.isFile())
19
+ return null;
20
+ const raw = await fs.readFile(p, 'utf-8');
21
+ const content = clipIf(path.basename(p), raw, budget);
22
+ if (!content)
23
+ return null;
24
+ return { path: p, content };
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ const AGENT_NAMES = ['AGENTS.md', 'agents.md', 'Agents.md'];
31
+ const MEMORY_NAMES = ['MEMORY.md', 'memory.md', 'Memory.md'];
32
+ async function findNamedFile(dir, names) {
33
+ for (const n of names) {
34
+ const p = path.join(dir, n);
35
+ const st = await fs.stat(p).catch(() => null);
36
+ if (st && st.isFile())
37
+ return p;
38
+ }
39
+ return null;
40
+ }
41
+ function dirChainFromRoot(from) {
42
+ const out = [];
43
+ let cur = path.resolve(from);
44
+ const root = path.parse(cur).root;
45
+ while (true) {
46
+ out.unshift(cur);
47
+ if (cur === root)
48
+ break;
49
+ const parent = path.dirname(cur);
50
+ if (parent === cur)
51
+ break;
52
+ cur = parent;
53
+ }
54
+ return out;
55
+ }
56
+ const PROJECT_SKILL_DIRS = ['.zames/skills', '.claude/skills', '.agents/skills', 'skills'];
57
+ function parseFrontmatter(raw) {
58
+ const norm = raw.replace(CRLF_RE, NL);
59
+ if (!norm.startsWith('---'))
60
+ return {};
61
+ const end = norm.indexOf(NL + '---', 3);
62
+ if (end === -1)
63
+ return {};
64
+ const block = norm.slice(3, end);
65
+ const out = {};
66
+ let lastKey = null;
67
+ for (const rawLine of block.split(NL)) {
68
+ const line = rawLine.replace(/\s+$/, '');
69
+ if (!line || /^\s*#/.test(line))
70
+ continue;
71
+ const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
72
+ if (m && !/^\s/.test(line)) {
73
+ const key = m[1];
74
+ let val = (m[2] || '').trim();
75
+ if (val === '|' || val === '>') {
76
+ out[key] = '';
77
+ lastKey = key;
78
+ continue;
79
+ }
80
+ if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
81
+ val = val.slice(1, -1);
82
+ }
83
+ out[key] = val;
84
+ lastKey = key;
85
+ }
86
+ else if (lastKey && /^\s+/.test(line)) {
87
+ out[lastKey] = ((out[lastKey] || '') + ' ' + line.trim()).trim();
88
+ }
89
+ }
90
+ const allowedTools = typeof out['allowed-tools'] === 'string'
91
+ ? out['allowed-tools'].split(/,/).map((s) => s.trim()).filter(Boolean)
92
+ : undefined;
93
+ const userInvokable = typeof out['user-invokable'] === 'string'
94
+ ? out['user-invokable'].toLowerCase() !== 'false'
95
+ : undefined;
96
+ return { name: out.name, description: out.description, allowedTools, userInvokable };
97
+ }
98
+ export function skillBody(raw) {
99
+ const norm = raw.replace(CRLF_RE, NL);
100
+ if (!norm.startsWith('---'))
101
+ return norm.trim();
102
+ const end = norm.indexOf(NL + '---', 3);
103
+ if (end === -1)
104
+ return norm.trim();
105
+ return norm.slice(norm.indexOf(NL, end + 1) + 1).trim();
106
+ }
107
+ async function findSkillFiles(root, depth = 3) {
108
+ const out = [];
109
+ const walk = async (dir, level) => {
110
+ let entries;
111
+ try {
112
+ entries = await fs.readdir(dir, { withFileTypes: true });
113
+ }
114
+ catch {
115
+ return;
116
+ }
117
+ for (const e of entries) {
118
+ const full = path.join(dir, e.name);
119
+ if (e.isFile() && e.name === 'SKILL.md')
120
+ out.push(full);
121
+ else if (e.isDirectory() && level < depth)
122
+ await walk(full, level + 1);
123
+ }
124
+ };
125
+ await walk(root, 1);
126
+ return out;
127
+ }
128
+ export async function loadSkills(workdir) {
129
+ const found = [];
130
+ const seen = new Set();
131
+ const roots = [];
132
+ for (const rel of PROJECT_SKILL_DIRS) {
133
+ roots.push({ dir: path.join(workdir, rel), source: 'project' });
134
+ }
135
+ roots.push({ dir: path.join(os.homedir(), '.zames', 'skills'), source: 'global' });
136
+ roots.push({ dir: path.join(os.homedir(), '.claude', 'skills'), source: 'global' });
137
+ for (const root of roots) {
138
+ const files = await findSkillFiles(root.dir);
139
+ for (const f of files) {
140
+ const raw = await fs.readFile(f, 'utf-8').catch(() => null);
141
+ if (raw === null)
142
+ continue;
143
+ const fm = parseFrontmatter(raw);
144
+ const dir = path.dirname(f);
145
+ const name = (fm.name || path.basename(dir)).trim();
146
+ if (!name)
147
+ continue;
148
+ const key = name.toLowerCase();
149
+ if (seen.has(key))
150
+ continue;
151
+ seen.add(key);
152
+ const description = (fm.description || '').replace(/\s+/g, ' ').trim();
153
+ found.push({ name, description, path: f, dir, allowedTools: fm.allowedTools, source: root.source, userInvokable: fm.userInvokable !== false });
154
+ }
155
+ }
156
+ found.sort((a, b) => a.name.localeCompare(b.name));
157
+ return found;
158
+ }
159
+ const PROJECT_COMMAND_DIRS = ['.zames/commands', '.claude/commands', '.agents/commands'];
160
+ async function loadCommandsFromDir(dir) {
161
+ const out = [];
162
+ let entries;
163
+ try {
164
+ entries = await fs.readdir(dir);
165
+ }
166
+ catch {
167
+ return out;
168
+ }
169
+ for (const e of entries) {
170
+ if (!/\.md$/i.test(e))
171
+ continue;
172
+ const full = path.join(dir, e);
173
+ const st = await fs.stat(full).catch(() => null);
174
+ if (!st || !st.isFile())
175
+ continue;
176
+ const raw = await fs.readFile(full, 'utf-8').catch(() => null);
177
+ if (raw === null)
178
+ continue;
179
+ const fm = parseFrontmatter(raw);
180
+ const name = path.basename(e, '.md').trim();
181
+ if (!name)
182
+ continue;
183
+ out.push({ name, description: (fm.description || '').replace(/\s+/g, ' ').trim(), path: full, body: skillBody(raw) });
184
+ }
185
+ return out;
186
+ }
187
+ export async function loadCommands(workdir) {
188
+ const out = [];
189
+ const seen = new Set();
190
+ const dirs = PROJECT_COMMAND_DIRS.map((d) => path.join(workdir, d));
191
+ dirs.push(path.join(os.homedir(), '.zames', 'commands'));
192
+ dirs.push(path.join(os.homedir(), '.claude', 'commands'));
193
+ for (const dir of dirs) {
194
+ for (const c of await loadCommandsFromDir(dir)) {
195
+ const key = c.name.toLowerCase();
196
+ if (seen.has(key))
197
+ continue;
198
+ seen.add(key);
199
+ out.push(c);
200
+ }
201
+ }
202
+ out.sort((a, b) => a.name.localeCompare(b.name));
203
+ return out;
204
+ }
205
+ async function loadAgentsChain(workdir) {
206
+ const out = [];
207
+ const seen = new Set();
208
+ for (const p of [path.join(os.homedir(), '.zames', 'AGENTS.md'), path.join(os.homedir(), '.claude', 'CLAUDE.md')]) {
209
+ const f = await readIfFile(p, MAX_FILE_BASE);
210
+ if (f && !seen.has(f.path)) {
211
+ seen.add(f.path);
212
+ out.push(f);
213
+ }
214
+ }
215
+ const chain = dirChainFromRoot(workdir);
216
+ for (const dir of chain) {
217
+ const p = await findNamedFile(dir, AGENT_NAMES);
218
+ if (!p || seen.has(p))
219
+ continue;
220
+ const f = await readIfFile(p, MAX_FILE_BASE);
221
+ if (f) {
222
+ seen.add(p);
223
+ out.push(f);
224
+ }
225
+ }
226
+ return out;
227
+ }
228
+ async function loadMemoryChain(workdir) {
229
+ const out = [];
230
+ const seen = new Set();
231
+ for (const p of [path.join(os.homedir(), '.zames', 'MEMORY.md'), path.join(os.homedir(), '.claude', 'MEMORY.md')]) {
232
+ const f = await readIfFile(p, MAX_FILE_BASE);
233
+ if (f && !seen.has(f.path)) {
234
+ seen.add(f.path);
235
+ out.push(f);
236
+ }
237
+ }
238
+ const chain = dirChainFromRoot(workdir);
239
+ for (const dir of chain) {
240
+ const p = await findNamedFile(dir, MEMORY_NAMES);
241
+ if (!p || seen.has(p))
242
+ continue;
243
+ const f = await readIfFile(p, MAX_FILE_BASE);
244
+ if (f) {
245
+ seen.add(p);
246
+ out.push(f);
247
+ }
248
+ }
249
+ return out;
250
+ }
251
+ export async function loadProjectContext(workdir) {
252
+ const [agents, memory, skills, commands] = await Promise.all([
253
+ loadAgentsChain(workdir).catch(() => []),
254
+ loadMemoryChain(workdir).catch(() => []),
255
+ loadSkills(workdir).catch(() => []),
256
+ loadCommands(workdir).catch(() => []),
257
+ ]);
258
+ let used = 0;
259
+ const withinBudget = (files) => {
260
+ const kept = [];
261
+ for (const f of files) {
262
+ if (used + f.content.length > MAX_TOTAL)
263
+ break;
264
+ used += f.content.length;
265
+ kept.push(f);
266
+ }
267
+ return kept;
268
+ };
269
+ return { agents: withinBudget(agents), memory: withinBudget(memory), skills, commands };
270
+ }
package/dist/i18n.js CHANGED
@@ -83,6 +83,22 @@ const CATALOG = {
83
83
  'help.cmd.debug_dom': { ru: '/debug-dom сохранить HTML страницы (для отладки)', en: '/debug-dom dump page HTML (debug)' },
84
84
  'help.cmd.help': { ru: '/help, help справка', en: '/help, help this help' },
85
85
  'help.cmd.exit': { ru: '/exit, /quit, exit выход', en: '/exit, /quit, exit exit' },
86
+ 'skills.none': { ru: 'Навыки не найдены. Добавьте SKILL.md в .zames/skills/<имя>/ или ~/.zames/skills/.', en: 'No skills found. Add SKILL.md under .zames/skills/<name>/ or ~/.zames/skills/.' },
87
+ 'skills.title': { ru: 'Найдено навыков: {n}', en: 'Skills found: {n}' },
88
+ 'memory.agents': { ru: 'Инструкции (AGENTS.md):', en: 'Instructions (AGENTS.md):' },
89
+ 'memory.memory': { ru: 'Память (MEMORY.md):', en: 'Memory (MEMORY.md):' },
90
+ 'init.exists': { ru: 'AGENTS.md уже существует: {v}', en: 'AGENTS.md already exists: {v}' },
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)' },
96
+ 'help.cmd.skill': { ru: 'навык (SKILL.md)', en: 'skill (SKILL.md)' },
97
+ 'help.cmd.custom': { ru: 'пользовательская команда', en: 'custom command' },
98
+ 'help.cmd.skills': { ru: '/skills список найденных навыков', en: '/skills list discovered skills' },
99
+ 'help.cmd.memory': { ru: '/memory показать MEMORY.md и AGENTS.md', en: '/memory show MEMORY.md and AGENTS.md' },
100
+ 'help.cmd.init': { ru: '/init [--force] изучить проект и создать AGENTS.md', en: '/init [--force] analyze the project and create AGENTS.md' },
101
+ 'help.skills': { ru: 'Навыки и свои команды:', en: 'Skills and custom commands:' },
86
102
  'help.self_review': { ru: 'Самообзор (отладка агента):', en: 'Self-review (agent debugging):' },
87
103
  'help.self.review': { ru: '/self-review [фокус] снять снапшот src/ и запустить ревью', en: '/self-review [focus] snapshot src/ and run review' },
88
104
  'help.self.fix': { ru: '/self-fix <name> [фокус] вернуться в существующий снапшот и продолжить', en: '/self-fix <name> [focus] enter an existing snapshot and continue' },
package/dist/index.js CHANGED
@@ -226,6 +226,7 @@ ${theme.bold(t('help.self_review'))}
226
226
  ${t('help.self.list')}
227
227
  ${t('help.self.diff')}
228
228
  ${t('help.self.apply')}
229
+ ${dynamicCommands.length ? '\n' + theme.bold(t('help.skills')) + '\n' + dynamicCommands.map((d) => ' ' + d.name.padEnd(24) + ' ' + d.description).join('\n') : ''}
229
230
 
230
231
  ${theme.bold(t('help.files'))}
231
232
  ${t('help.files.logs')} ${config.transcript.dir}
@@ -261,6 +262,9 @@ const SLASH_COMMANDS = [
261
262
  { name: '/undo-list', key: 'help.cmd.undo_list' },
262
263
  { name: '/transcript', key: 'help.cmd.transcript' },
263
264
  { name: '/config', key: 'help.cmd.config' },
265
+ { name: '/skills', key: 'help.cmd.skills' },
266
+ { name: '/memory', key: 'help.cmd.memory' },
267
+ { name: '/init', key: 'help.cmd.init' },
264
268
  { name: '/debug-dom', key: 'help.cmd.debug_dom' },
265
269
  { name: '/self-review', key: 'help.self.review' },
266
270
  { name: '/self-fix', key: 'help.self.fix' },
@@ -271,12 +275,82 @@ const SLASH_COMMANDS = [
271
275
  { name: '/exit', key: 'help.cmd.exit' },
272
276
  { name: '/quit', key: 'help.cmd.exit' },
273
277
  ];
278
+ // Dynamically discovered skills and custom commands for the current workdir.
279
+ // Filled in by refreshDynamicCommands() and shown in the «/» hint list and
280
+ // help. Skills are surfaced as /skill-name entries (user-invokable only).
281
+ let dynamicCommands = [];
282
+ async function refreshDynamicCommands(workdir) {
283
+ const out = [];
284
+ try {
285
+ const { loadSkills, loadCommands } = await import('./context.js');
286
+ const skills = await loadSkills(workdir);
287
+ for (const s of skills) {
288
+ if (!s.userInvokable)
289
+ continue;
290
+ out.push({
291
+ name: '/' + s.name,
292
+ description: s.description || t('help.cmd.skill'),
293
+ });
294
+ }
295
+ const cmds = await loadCommands(workdir);
296
+ for (const c of cmds) {
297
+ out.push({
298
+ name: '/' + c.name,
299
+ description: c.description || t('help.cmd.custom'),
300
+ });
301
+ }
302
+ }
303
+ catch {
304
+ // best-effort: a broken skill/command must not break the CLI
305
+ }
306
+ dynamicCommands = out;
307
+ }
308
+ // Expand a slash target (skill or custom command) into a task string.
309
+ // Returns null when the name is neither a skill nor a custom command.
310
+ async function expandSlashTarget(workdir, name, rest) {
311
+ try {
312
+ const { loadSkills, loadCommands, skillBody } = await import('./context.js');
313
+ const commands = await loadCommands(workdir);
314
+ const cmd = commands.find((c) => c.name.toLowerCase() === name.toLowerCase());
315
+ if (cmd) {
316
+ let body = cmd.body.replace(/\{\{args\}\}/g, rest);
317
+ body = body.replace(/\$ARGUMENTS/g, rest);
318
+ return body.trim() || rest;
319
+ }
320
+ const skills = await loadSkills(workdir);
321
+ const skill = skills.find((s) => s.name.toLowerCase() === name.toLowerCase() && s.userInvokable);
322
+ if (skill) {
323
+ let body = '';
324
+ try {
325
+ const raw = await fs.readFile(skill.path, 'utf-8');
326
+ body = skillBody(raw);
327
+ }
328
+ catch {
329
+ body = '';
330
+ }
331
+ const header = 'Follow the skill "' + skill.name + '" (from ' + skill.path + ').';
332
+ const extra = rest ? '\n\nAdditional instructions from the operator: ' + rest : '';
333
+ return header + '\n\n' + body + extra;
334
+ }
335
+ }
336
+ catch {
337
+ // fall through: treated as an unknown command
338
+ }
339
+ return null;
340
+ }
274
341
  // Slash-command descriptions in the current language (for LineEditor hints).
275
342
  function buildSlashCommands() {
276
- return SLASH_COMMANDS.map((c) => ({
343
+ const base = SLASH_COMMANDS.map((c) => ({
277
344
  name: c.name,
278
345
  description: t(c.key),
279
346
  }));
347
+ const known = new Set(base.map((c) => c.name.toLowerCase()));
348
+ for (const d of dynamicCommands) {
349
+ if (known.has(d.name.toLowerCase()))
350
+ continue;
351
+ base.push(d);
352
+ }
353
+ return base;
280
354
  }
281
355
  // We put the agent's temporary files (one-off scripts, etc.) in
282
356
  // <project>/tmp — this folder is in .gitignore and is cleaned on every launch.
@@ -973,6 +1047,7 @@ async function main() {
973
1047
  }
974
1048
  return theme.prompt('❯ ') + tail + theme.dim(' › ');
975
1049
  };
1050
+ await refreshDynamicCommands(currentWorkdir);
976
1051
  if (process.stdin.isTTY && process.stdout.isTTY) {
977
1052
  const ed = new LineEditor({
978
1053
  prompt: buildPrompt(),
@@ -1677,6 +1752,127 @@ async function main() {
1677
1752
  console.log(theme.system(currentWorkdir));
1678
1753
  continue;
1679
1754
  }
1755
+ if (lower === '/skills') {
1756
+ const { loadSkills } = await import('./context.js');
1757
+ const skills = await loadSkills(currentWorkdir);
1758
+ if (!skills.length) {
1759
+ console.log(theme.dim(t('skills.none')));
1760
+ }
1761
+ else {
1762
+ console.log(theme.system(t('skills.title', { n: String(skills.length) })));
1763
+ for (const s of skills) {
1764
+ console.log(' ' +
1765
+ theme.user('/' + s.name) +
1766
+ theme.dim(' [' + s.source + '] ') +
1767
+ (s.description || theme.dim(t('common.none'))));
1768
+ console.log(theme.dim(' ' + s.path));
1769
+ }
1770
+ }
1771
+ continue;
1772
+ }
1773
+ if (lower === '/memory') {
1774
+ const { loadProjectContext } = await import('./context.js');
1775
+ const ctx = await loadProjectContext(currentWorkdir);
1776
+ const show = (title, files) => {
1777
+ console.log(theme.system(title));
1778
+ if (!files.length) {
1779
+ console.log(theme.dim(' ' + t('common.none')));
1780
+ return;
1781
+ }
1782
+ for (const f of files)
1783
+ console.log(' ' + f.path);
1784
+ };
1785
+ show(t('memory.agents'), ctx.agents);
1786
+ show(t('memory.memory'), ctx.memory);
1787
+ continue;
1788
+ }
1789
+ if (lower === '/init' || lower.startsWith('/init ')) {
1790
+ const force = lower.includes('--force');
1791
+ const target = path.join(currentWorkdir, 'AGENTS.md');
1792
+ const exists = await fs.stat(target).catch(() => null);
1793
+ if (exists && !force) {
1794
+ console.log(theme.warn(t('init.overwrite', { v: target })));
1795
+ continue;
1796
+ }
1797
+ // Like Codex: let the agent explore the project and write AGENTS.md via
1798
+ // the Write tool, so the file reflects the real build/test commands and
1799
+ // conventions instead of a static template.
1800
+ const initTask = 'The user ran /init. Analyze this repository and create an AGENTS.md ' +
1801
+ 'file at the project root that onboards future coding agents. Explore ' +
1802
+ 'the project first (read the README, package.json and other manifests, ' +
1803
+ 'configs, CI, and a sample of source files). Then write AGENTS.md with: ' +
1804
+ 'an overview of what the project is; build/typecheck/test/lint commands ' +
1805
+ '(use the real scripts you find); code style and conventions; the repo ' +
1806
+ 'layout; and any gotchas or rules an agent must follow. Keep it concise ' +
1807
+ 'and factual, based only on what you find. Write the file with the Write ' +
1808
+ 'tool, then reply via respond with a one-line summary.';
1809
+ const initTools = mod.createTools(currentWorkdir, { undo });
1810
+ if (editor)
1811
+ editor.busy = true;
1812
+ console.log(theme.system(t('init.analyzing')));
1813
+ try {
1814
+ await mod.runAgentLoop({
1815
+ browser,
1816
+ tools: initTools,
1817
+ task: initTask,
1818
+ workdir: currentWorkdir,
1819
+ maxIterations: maxIter,
1820
+ freshChat: freshChatNext,
1821
+ sendSystemPrompt: sendSystemPromptNext,
1822
+ transcript,
1823
+ onThinking: () => { },
1824
+ onToolCall: (name, toolArgs) => {
1825
+ if (editor)
1826
+ editor.toolCall(name, toolArgs);
1827
+ },
1828
+ onToolResult: (r) => {
1829
+ if (editor)
1830
+ editor.toolResult(r);
1831
+ },
1832
+ onAssistantMessage: (m) => {
1833
+ if (editor)
1834
+ editor.assistant(m);
1835
+ },
1836
+ onWarning: (m) => {
1837
+ if (editor)
1838
+ editor.warning(m);
1839
+ },
1840
+ locale: currentLocale,
1841
+ });
1842
+ }
1843
+ catch (e) {
1844
+ console.error(theme.error(t('init.failed')), e.message);
1845
+ }
1846
+ finally {
1847
+ if (editor)
1848
+ editor.busy = false;
1849
+ }
1850
+ freshChatNext = false;
1851
+ sendSystemPromptNext = false;
1852
+ if (!currentChatId) {
1853
+ currentChatId = await browser.getCurrentChatId();
1854
+ }
1855
+ saveLastChat(currentChatId, currentWorkdir);
1856
+ const created = await fs.stat(target).catch(() => null);
1857
+ if (created) {
1858
+ console.log(theme.assistant(t('init.done', { v: target })));
1859
+ }
1860
+ else {
1861
+ const stub = '# AGENTS.md\n\n' +
1862
+ 'Project instructions for the coding agent. Describe the build/test commands, ' +
1863
+ 'conventions, and any rules the agent must follow in this repository.\n\n' +
1864
+ '## Commands\n\n' +
1865
+ '- build: `...`\n' +
1866
+ '- test: `...`\n' +
1867
+ '- lint: `...`\n\n' +
1868
+ '## Conventions\n\n' +
1869
+ '- ...\n';
1870
+ await fs.writeFile(target, stub, 'utf-8');
1871
+ console.log(theme.warn(t('init.failed')));
1872
+ console.log(theme.assistant(t('init.created', { v: target })));
1873
+ }
1874
+ continue;
1875
+ }
1680
1876
  if (lower === '/reload') {
1681
1877
  console.log(theme.system(t('msg.reload_start')));
1682
1878
  try {
@@ -1693,6 +1889,10 @@ async function main() {
1693
1889
  catch (e) {
1694
1890
  console.error(theme.error(t('msg.reload_error')), e.message);
1695
1891
  }
1892
+ await autoReload();
1893
+ await refreshDynamicCommands(currentWorkdir);
1894
+ if (editor)
1895
+ editor.setCommands(buildSlashCommands());
1696
1896
  continue;
1697
1897
  }
1698
1898
  if (lower === '/status') {
@@ -1803,6 +2003,9 @@ async function main() {
1803
2003
  currentWorkdir = newDir;
1804
2004
  freshChatNext = true;
1805
2005
  sendSystemPromptNext = true;
2006
+ await refreshDynamicCommands(currentWorkdir);
2007
+ if (editor)
2008
+ editor.setCommands(buildSlashCommands());
1806
2009
  console.log(theme.system(t('cd.changed', { v: newDir })));
1807
2010
  }
1808
2011
  catch (e) {
@@ -1810,19 +2013,29 @@ async function main() {
1810
2013
  }
1811
2014
  continue;
1812
2015
  }
2016
+ // Custom command or skill invoked as a slash command? Expand it into a
2017
+ // task. Skills are instructions the agent follows; custom commands are
2018
+ // prompt templates with $ARGUMENTS / {{args}} placeholders.
2019
+ let expandedTask = null;
1813
2020
  if (lower.startsWith('/')) {
2021
+ const name = trimmed.slice(1).split(/\s+/)[0];
2022
+ const rest = trimmed.slice(1 + name.length).trim();
2023
+ expandedTask = await expandSlashTarget(currentWorkdir, name, rest);
2024
+ }
2025
+ if (lower.startsWith('/') && expandedTask === null) {
1814
2026
  console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
1815
2027
  continue;
1816
2028
  }
1817
2029
  // ---- Regular task (including in review mode) ----
2030
+ const taskText = expandedTask !== null ? expandedTask : trimmed;
1818
2031
  // Dev mode: pick up fresh logic modules before the task.
1819
2032
  await autoReload();
1820
- transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
2033
+ transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
1821
2034
  const tools = mod.createTools(currentWorkdir, { undo });
1822
2035
  if (editor)
1823
2036
  editor.busy = true;
1824
2037
  try {
1825
- await runTask(browser, tools, trimmed, currentWorkdir, {
2038
+ await runTask(browser, tools, taskText, currentWorkdir, {
1826
2039
  transcript,
1827
2040
  freshChat: freshChatNext,
1828
2041
  sendSystemPrompt: sendSystemPromptNext,
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;
@@ -1,5 +1,53 @@
1
1
  import { translate } from './i18n.js';
2
- export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], }) {
2
+ /** Render the AGENTS.md / MEMORY / skills / commands blocks of the prompt. */
3
+ export function renderContextSection(context) {
4
+ if (!context)
5
+ return '';
6
+ let out = '';
7
+ for (const f of context.agents) {
8
+ out +=
9
+ '\n## Project instructions (' + f.path + ')\n\n' +
10
+ 'The following come from ' + f.path + '. Follow them for this project unless the operator explicitly overrides them.\n\n' +
11
+ f.content +
12
+ '\n';
13
+ }
14
+ for (const f of context.memory) {
15
+ out +=
16
+ '\n## Memory (' + f.path + ')\n\n' +
17
+ 'Durable notes accumulated across sessions. Treat as background knowledge; update this file when you learn something worth remembering.\n\n' +
18
+ f.content +
19
+ '\n';
20
+ }
21
+ if (context.memory.length) {
22
+ out +=
23
+ '\nWhen you learn a durable fact about this project or the operator preferences (build quirks, ' +
24
+ 'conventions, gotchas), append a short bullet to ' + context.memory[0].path + ' via Edit/Write so it survives ' +
25
+ 'into future sessions. Keep entries concise.\n';
26
+ }
27
+ if (context.skills.length) {
28
+ const lines = context.skills.map((s) => {
29
+ const tools = s.allowedTools && s.allowedTools.length
30
+ ? ' [tools: ' + s.allowedTools.join(', ') + ']'
31
+ : '';
32
+ return '- ' + s.name + tools + ': ' + (s.description || '(no description)') + '\n file: ' + s.path;
33
+ });
34
+ out +=
35
+ '\n## Skills\n\n' +
36
+ 'Specialized workflows available as SKILL.md files. When a task matches a skill description, read its file and follow its instructions. Skills are progressive disclosure: only read the body when the skill applies.\n\n' +
37
+ lines.join('\n') +
38
+ '\n';
39
+ }
40
+ if (context.commands.length) {
41
+ const lines = context.commands.map((c) => '- /' + c.name + ': ' + (c.description || '(no description)'));
42
+ out +=
43
+ '\n## Custom commands\n\n' +
44
+ 'Reusable prompts the operator can invoke with a slash command. If the operator runs one, its body arrives as the task text.\n\n' +
45
+ lines.join('\n') +
46
+ '\n';
47
+ }
48
+ return out;
49
+ }
50
+ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], context = null, }) {
3
51
  const t = translate(locale);
4
52
  const toolDescriptions = tools
5
53
  .map((t2) => `### ${t2.name}\n${t2.description}\nПараметры: ${JSON.stringify(t2.parameters)}`)
@@ -11,6 +59,7 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
11
59
  ? `\n## Attachments\n\nThe user attached files to this task: ${attachments.join(', ')}.\n` +
12
60
  `Each [image#N] / [file#N] marker corresponds to a file the user pasted into the terminal; the file was uploaded to the chat and also saved under <project>/tmp. Look at the images in the chat; for files, read the copy in tmp if you need the contents.\n`
13
61
  : '';
62
+ const contextSection = renderContextSection(context);
14
63
  return `You are a coding agent running in a terminal. You help the user with software engineering tasks by reading files, writing code, running commands, and iterating until the task is done.
15
64
 
16
65
  You work in the directory: ${workdir}
@@ -63,7 +112,7 @@ task is done (or when you must ask the operator), not between tool calls.
63
112
  You have access to the following tools:
64
113
 
65
114
  ${toolDescriptions}
66
- ${gitSection}${attachSection}
115
+ ${gitSection}${attachSection}${contextSection}
67
116
  ## How to use tools
68
117
 
69
118
  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.10.9",
3
+ "version": "2.12.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {