zames_pro 2.10.9 → 2.11.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,35 @@ 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`). Create a starter file with
87
+ `/init`.
88
+ - **MEMORY.md** — durable notes that persist between sessions. The agent
89
+ appends useful facts here; you can edit it by hand.
90
+ - **Skills** — a folder with a `SKILL.md` file (YAML frontmatter: `name`,
91
+ `description`, optional `allowed-tools`, `user-invokable`) plus any helper
92
+ files. Discovered under `.zames/skills/`, `.claude/skills/`,
93
+ `.agents/skills/`, `skills/`, and `~/.zames/skills/`. The agent reads the
94
+ body only when a task matches the description. List them with `/skills`;
95
+ invoke one with `/<skill-name>`.
96
+ - **Custom commands** — `.md` files under `.zames/commands/` (or
97
+ `.claude/commands/`). They support `$ARGUMENTS` / `{{args}}` placeholders and
98
+ are invoked with `/<command-name>`.
99
+
100
+ Skills and custom commands show up in the «/» completion list and in `/help`.
101
+
102
+ ```
103
+ /skills list discovered skills
104
+ /memory show AGENTS.md / MEMORY.md in effect
105
+ /init create a starter AGENTS.md
106
+ ```
107
+
79
108
  ## Configuration
80
109
 
81
110
  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).
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,18 @@ 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
+ 'help.cmd.skill': { ru: 'навык (SKILL.md)', en: 'skill (SKILL.md)' },
93
+ 'help.cmd.custom': { ru: 'пользовательская команда', en: 'custom command' },
94
+ 'help.cmd.skills': { ru: '/skills список найденных навыков', en: '/skills list discovered skills' },
95
+ 'help.cmd.memory': { ru: '/memory показать MEMORY.md и AGENTS.md', en: '/memory show MEMORY.md and AGENTS.md' },
96
+ 'help.cmd.init': { ru: '/init создать AGENTS.md для проекта', en: '/init create AGENTS.md for the project' },
97
+ 'help.skills': { ru: 'Навыки и свои команды:', en: 'Skills and custom commands:' },
86
98
  'help.self_review': { ru: 'Самообзор (отладка агента):', en: 'Self-review (agent debugging):' },
87
99
  'help.self.review': { ru: '/self-review [фокус] снять снапшот src/ и запустить ревью', en: '/self-review [focus] snapshot src/ and run review' },
88
100
  '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,60 @@ 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') {
1790
+ const target = path.join(currentWorkdir, 'AGENTS.md');
1791
+ const exists = await fs.stat(target).catch(() => null);
1792
+ if (exists) {
1793
+ console.log(theme.warn(t('init.exists', { v: target })));
1794
+ continue;
1795
+ }
1796
+ const stub = '# AGENTS.md\n\n' +
1797
+ 'Project instructions for the coding agent. Describe the build/test commands, ' +
1798
+ 'conventions, and any rules the agent must follow in this repository.\n\n' +
1799
+ '## Commands\n\n' +
1800
+ '- build: `...`\n' +
1801
+ '- test: `...`\n' +
1802
+ '- lint: `...`\n\n' +
1803
+ '## Conventions\n\n' +
1804
+ '- ...\n';
1805
+ await fs.writeFile(target, stub, 'utf-8');
1806
+ console.log(theme.assistant(t('init.created', { v: target })));
1807
+ continue;
1808
+ }
1680
1809
  if (lower === '/reload') {
1681
1810
  console.log(theme.system(t('msg.reload_start')));
1682
1811
  try {
@@ -1693,6 +1822,10 @@ async function main() {
1693
1822
  catch (e) {
1694
1823
  console.error(theme.error(t('msg.reload_error')), e.message);
1695
1824
  }
1825
+ await autoReload();
1826
+ await refreshDynamicCommands(currentWorkdir);
1827
+ if (editor)
1828
+ editor.setCommands(buildSlashCommands());
1696
1829
  continue;
1697
1830
  }
1698
1831
  if (lower === '/status') {
@@ -1803,6 +1936,9 @@ async function main() {
1803
1936
  currentWorkdir = newDir;
1804
1937
  freshChatNext = true;
1805
1938
  sendSystemPromptNext = true;
1939
+ await refreshDynamicCommands(currentWorkdir);
1940
+ if (editor)
1941
+ editor.setCommands(buildSlashCommands());
1806
1942
  console.log(theme.system(t('cd.changed', { v: newDir })));
1807
1943
  }
1808
1944
  catch (e) {
@@ -1810,19 +1946,29 @@ async function main() {
1810
1946
  }
1811
1947
  continue;
1812
1948
  }
1949
+ // Custom command or skill invoked as a slash command? Expand it into a
1950
+ // task. Skills are instructions the agent follows; custom commands are
1951
+ // prompt templates with $ARGUMENTS / {{args}} placeholders.
1952
+ let expandedTask = null;
1813
1953
  if (lower.startsWith('/')) {
1954
+ const name = trimmed.slice(1).split(/\s+/)[0];
1955
+ const rest = trimmed.slice(1 + name.length).trim();
1956
+ expandedTask = await expandSlashTarget(currentWorkdir, name, rest);
1957
+ }
1958
+ if (lower.startsWith('/') && expandedTask === null) {
1814
1959
  console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
1815
1960
  continue;
1816
1961
  }
1817
1962
  // ---- Regular task (including in review mode) ----
1963
+ const taskText = expandedTask !== null ? expandedTask : trimmed;
1818
1964
  // Dev mode: pick up fresh logic modules before the task.
1819
1965
  await autoReload();
1820
- transcript.log('user_task', { task: trimmed, workdir: currentWorkdir });
1966
+ transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
1821
1967
  const tools = mod.createTools(currentWorkdir, { undo });
1822
1968
  if (editor)
1823
1969
  editor.busy = true;
1824
1970
  try {
1825
- await runTask(browser, tools, trimmed, currentWorkdir, {
1971
+ await runTask(browser, tools, taskText, currentWorkdir, {
1826
1972
  transcript,
1827
1973
  freshChat: freshChatNext,
1828
1974
  sendSystemPrompt: sendSystemPromptNext,
@@ -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.11.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {