zames_pro 2.10.8 → 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 +29 -0
- package/dist/agent-loop.js +22 -2
- package/dist/browser.js +16 -0
- package/dist/config.js +2 -2
- package/dist/context.js +270 -0
- package/dist/i18n.js +13 -0
- package/dist/index.js +200 -5
- package/dist/input.js +42 -0
- package/dist/system-prompt.js +51 -2
- package/package.json +1 -1
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`
|
package/dist/agent-loop.js
CHANGED
|
@@ -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 =
|
|
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
|
|
@@ -20,6 +21,11 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
20
21
|
};
|
|
21
22
|
};
|
|
22
23
|
const safeThinking = safe(onThinking);
|
|
24
|
+
// Start the "working" indicator ONLY when a message is really sent (after
|
|
25
|
+
// the send-pause), not while the loop is still preparing the request.
|
|
26
|
+
// Previously safeThinking() fired before browser.ask(), so the spinner ran
|
|
27
|
+
// through the whole throttle pause / chat-open phase with nothing in flight.
|
|
28
|
+
browser.onSendStart = safeThinking;
|
|
23
29
|
const safeAssistantThought = safe(onAssistantThought);
|
|
24
30
|
const safeToolCall = safe(onToolCall);
|
|
25
31
|
const safeToolResult = safe(onToolResult);
|
|
@@ -54,15 +60,27 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
54
60
|
catch (e) {
|
|
55
61
|
gitText = `(git context error: ${e.message})`;
|
|
56
62
|
}
|
|
63
|
+
let context = null;
|
|
64
|
+
try {
|
|
65
|
+
context = await loadProjectContext(workdir);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
context = null;
|
|
69
|
+
}
|
|
57
70
|
const systemPrompt = buildSystemPrompt({
|
|
58
71
|
workdir,
|
|
59
72
|
tools,
|
|
60
73
|
gitContext: gitText,
|
|
61
74
|
locale,
|
|
75
|
+
context,
|
|
62
76
|
});
|
|
63
77
|
transcript?.log('system_prompt', {
|
|
64
78
|
length: systemPrompt.length,
|
|
65
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) ?? [],
|
|
66
84
|
});
|
|
67
85
|
safeThinking();
|
|
68
86
|
// system-prompt is an agent send: throttled (agent: true).
|
|
@@ -134,7 +152,9 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
|
|
|
134
152
|
let afterToolRetries = 0;
|
|
135
153
|
const MAX_AFTER_TOOL_RETRIES = 6;
|
|
136
154
|
for (let i = 0; i < maxIterations; i++) {
|
|
137
|
-
|
|
155
|
+
// NOTE: the spinner is NOT started here. browser.onSendStart fires it
|
|
156
|
+
// right when the message is actually typed/sent (after the send-pause),
|
|
157
|
+
// so no spinner runs during the pre-send phase.
|
|
138
158
|
// The first message (task) is user input: no throttle.
|
|
139
159
|
// Subsequent ones (tool-result and resend requests) are agent sends:
|
|
140
160
|
// throttled so we don't hit the rate limit.
|
package/dist/browser.js
CHANGED
|
@@ -139,6 +139,11 @@ export class DeepSeekBrowser {
|
|
|
139
139
|
_netSniff;
|
|
140
140
|
_netSniffLimit;
|
|
141
141
|
_netHookInstalled;
|
|
142
|
+
// Fired right when a message is actually typed/sent (AFTER the send-pause
|
|
143
|
+
// and attachments). Used to start the "agent is working" spinner only when
|
|
144
|
+
// a generation really begins, not during the pre-send phase (chat open,
|
|
145
|
+
// throttle wait), which used to show a spinner with no work in flight.
|
|
146
|
+
onSendStart;
|
|
142
147
|
constructor({ headless = false, debug = false, channel = 'chrome', answerTimeoutMs = 180000, askRetries = 3, stabilityChecks = 3, stabilityDelayMs = 1000, minSendIntervalMs = 15000, rateLimitWaitMs = 300000, maxRateLimitRetries = 6, maxServerBusyRetries = 5, serverBusyWaitMs = 3000, } = {}) {
|
|
143
148
|
this.headless = headless;
|
|
144
149
|
this.debug = debug;
|
|
@@ -161,6 +166,7 @@ export class DeepSeekBrowser {
|
|
|
161
166
|
this._netSniff = [];
|
|
162
167
|
this._netSniffLimit = 5;
|
|
163
168
|
this._netHookInstalled = false;
|
|
169
|
+
this.onSendStart = null;
|
|
164
170
|
}
|
|
165
171
|
async launch() {
|
|
166
172
|
await killStaleChrome();
|
|
@@ -670,6 +676,16 @@ export class DeepSeekBrowser {
|
|
|
670
676
|
// Esc/Ctrl+C pressed during the pause — do not send anything.
|
|
671
677
|
if (this._abort)
|
|
672
678
|
return '(прервано пользователем)';
|
|
679
|
+
// The pause is over and we are really about to type/send: only NOW start
|
|
680
|
+
// the "working" indicator. The spinner used to be started by the caller
|
|
681
|
+
// BEFORE ask(), so it ran for the whole throttle pause and chat-opening
|
|
682
|
+
// phase with no generation in flight.
|
|
683
|
+
if (this.onSendStart) {
|
|
684
|
+
try {
|
|
685
|
+
this.onSendStart();
|
|
686
|
+
}
|
|
687
|
+
catch { }
|
|
688
|
+
}
|
|
673
689
|
this._netCapture = '';
|
|
674
690
|
this._netCaptureAt = 0;
|
|
675
691
|
// Attach files/images FIRST (before the text): the DeepSeek upload widget
|
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:
|
|
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:
|
|
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' },
|
package/dist/context.js
ADDED
|
@@ -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' },
|
|
@@ -128,6 +140,7 @@ const CATALOG = {
|
|
|
128
140
|
'msg.new_chat': { ru: 'Создаю новый чат...', en: 'Creating a new chat...' },
|
|
129
141
|
'msg.new_chat_ok': { ru: 'Новый чат.', en: 'New chat.' },
|
|
130
142
|
'msg.resuming': { ru: 'Восстанавливаю сессию {id}...', en: 'Restoring session {id}...' },
|
|
143
|
+
'msg.input_locked': { ru: 'Идёт операция, ввод временно заблокирован…', en: 'Operation in progress, input is temporarily locked…' },
|
|
131
144
|
'msg.opening_chat': { ru: 'Открываю чат {id}...', en: 'Opening chat {id}...' },
|
|
132
145
|
'msg.chat_opened': { ru: 'Чат открыт.', en: 'Chat opened.' },
|
|
133
146
|
'msg.interactive': { ru: 'Интерактивный режим. Введите задачу. Команды — /help. Выход — /exit.', en: 'Interactive mode. Enter a task. Commands — /help. Exit — /exit.' },
|
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
|
-
|
|
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.
|
|
@@ -714,9 +788,15 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
714
788
|
};
|
|
715
789
|
// Execute the task, then everything the user managed to type while it
|
|
716
790
|
// ran. The queue may be replenished right during draining.
|
|
791
|
+
//
|
|
792
|
+
// NOTE: we do NOT call ui.thinking() here. runAgentLoop() fires
|
|
793
|
+
// onThinking() right before the actual browser send (after the send
|
|
794
|
+
// pause, system-prompt, etc.), so the spinner only appears when a
|
|
795
|
+
// generation really starts. Calling it here made the spinner run for
|
|
796
|
+
// the whole pre-send phase (chat creation, throttle wait) with no
|
|
797
|
+
// generation in flight.
|
|
717
798
|
while (true) {
|
|
718
|
-
|
|
719
|
-
await mod.runAgentLoop({
|
|
799
|
+
const outcome = await mod.runAgentLoop({
|
|
720
800
|
browser,
|
|
721
801
|
tools,
|
|
722
802
|
task: next.text,
|
|
@@ -735,6 +815,16 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
735
815
|
debugLog: debug,
|
|
736
816
|
locale: currentLocale,
|
|
737
817
|
});
|
|
818
|
+
// The loop may end WITHOUT a model answer: an exhausted iteration
|
|
819
|
+
// limit or an ask() watchdog (the model stopped responding). In that
|
|
820
|
+
// case no assistant message was shown, and the operator saw the run
|
|
821
|
+
// just "stop" after a tool call with no explanation. Surface it.
|
|
822
|
+
if (outcome &&
|
|
823
|
+
(outcome.startsWith('Достигнут лимит итераций') ||
|
|
824
|
+
outcome.startsWith('ask() watchdog'))) {
|
|
825
|
+
ui.warning(outcome);
|
|
826
|
+
transcript?.log('agent_no_answer', { outcome });
|
|
827
|
+
}
|
|
738
828
|
// Aborted (Esc/Ctrl+C) — we don't start the next tasks from the queue
|
|
739
829
|
// and clear it, so "stop" really stops everything.
|
|
740
830
|
if (browser._stopped) {
|
|
@@ -770,6 +860,9 @@ async function runTask(browser, tools, taskText, workdir, opts, attachments = []
|
|
|
770
860
|
finally {
|
|
771
861
|
stopWatching();
|
|
772
862
|
ui.stop();
|
|
863
|
+
// Detach the send hook so a later browser.ask() outside this task cannot
|
|
864
|
+
// start a stale spinner.
|
|
865
|
+
browser.onSendStart = null;
|
|
773
866
|
}
|
|
774
867
|
}
|
|
775
868
|
// ---------- main ----------
|
|
@@ -954,6 +1047,7 @@ async function main() {
|
|
|
954
1047
|
}
|
|
955
1048
|
return theme.prompt('❯ ') + tail + theme.dim(' › ');
|
|
956
1049
|
};
|
|
1050
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
957
1051
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
958
1052
|
const ed = new LineEditor({
|
|
959
1053
|
prompt: buildPrompt(),
|
|
@@ -1339,6 +1433,8 @@ async function main() {
|
|
|
1339
1433
|
}
|
|
1340
1434
|
if (['/new', '/clear', 'new'].includes(lower)) {
|
|
1341
1435
|
console.log(theme.system(t('msg.new_chat')));
|
|
1436
|
+
if (editor)
|
|
1437
|
+
editor.lock(t('msg.input_locked'));
|
|
1342
1438
|
try {
|
|
1343
1439
|
await browser.newChat();
|
|
1344
1440
|
freshChatNext = false;
|
|
@@ -1351,6 +1447,10 @@ async function main() {
|
|
|
1351
1447
|
catch (e) {
|
|
1352
1448
|
console.error(theme.error(t('msg.new_chat_error', { v: e.message })), e.message);
|
|
1353
1449
|
}
|
|
1450
|
+
finally {
|
|
1451
|
+
if (editor)
|
|
1452
|
+
editor.unlock();
|
|
1453
|
+
}
|
|
1354
1454
|
continue;
|
|
1355
1455
|
}
|
|
1356
1456
|
// ---------- Self-review ----------
|
|
@@ -1360,6 +1460,8 @@ async function main() {
|
|
|
1360
1460
|
const originalWorkdir = reviewMode
|
|
1361
1461
|
? reviewMode.originalWorkdir
|
|
1362
1462
|
: currentWorkdir;
|
|
1463
|
+
if (editor)
|
|
1464
|
+
editor.lock(t('msg.input_locked'));
|
|
1363
1465
|
try {
|
|
1364
1466
|
const result = await mod.selfReview({
|
|
1365
1467
|
browser,
|
|
@@ -1388,6 +1490,10 @@ async function main() {
|
|
|
1388
1490
|
if (debug)
|
|
1389
1491
|
console.error(e.stack);
|
|
1390
1492
|
}
|
|
1493
|
+
finally {
|
|
1494
|
+
if (editor)
|
|
1495
|
+
editor.unlock();
|
|
1496
|
+
}
|
|
1391
1497
|
continue;
|
|
1392
1498
|
}
|
|
1393
1499
|
if (lower === '/self-fix' || lower.startsWith('/self-fix ')) {
|
|
@@ -1513,6 +1619,8 @@ async function main() {
|
|
|
1513
1619
|
// ---------- Regular commands ----------
|
|
1514
1620
|
if (lower === '/chats') {
|
|
1515
1621
|
const spin = editor || mod.createSpinner(currentLocale);
|
|
1622
|
+
if (editor)
|
|
1623
|
+
editor.lock(t('msg.input_locked'));
|
|
1516
1624
|
spin.thinking();
|
|
1517
1625
|
try {
|
|
1518
1626
|
lastChats = await browser.listChats(30);
|
|
@@ -1533,6 +1641,10 @@ async function main() {
|
|
|
1533
1641
|
spin.stop();
|
|
1534
1642
|
console.error(theme.error(t('chats.fetch_error')), e.message);
|
|
1535
1643
|
}
|
|
1644
|
+
finally {
|
|
1645
|
+
if (editor)
|
|
1646
|
+
editor.unlock();
|
|
1647
|
+
}
|
|
1536
1648
|
continue;
|
|
1537
1649
|
}
|
|
1538
1650
|
if (lower === '/resume' || lower.startsWith('/resume ')) {
|
|
@@ -1556,6 +1668,8 @@ async function main() {
|
|
|
1556
1668
|
continue;
|
|
1557
1669
|
}
|
|
1558
1670
|
console.log(theme.system(t('chats.opening', { v: pick.title })));
|
|
1671
|
+
if (editor)
|
|
1672
|
+
editor.lock(t('msg.input_locked'));
|
|
1559
1673
|
try {
|
|
1560
1674
|
await browser.openChat(pick.id);
|
|
1561
1675
|
currentChatId = pick.id;
|
|
@@ -1571,6 +1685,10 @@ async function main() {
|
|
|
1571
1685
|
catch (e) {
|
|
1572
1686
|
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1573
1687
|
}
|
|
1688
|
+
finally {
|
|
1689
|
+
if (editor)
|
|
1690
|
+
editor.unlock();
|
|
1691
|
+
}
|
|
1574
1692
|
continue;
|
|
1575
1693
|
}
|
|
1576
1694
|
if (lower === '/chat') {
|
|
@@ -1608,6 +1726,8 @@ async function main() {
|
|
|
1608
1726
|
console.error(theme.error(t('sessions.resume_id_usage')));
|
|
1609
1727
|
continue;
|
|
1610
1728
|
}
|
|
1729
|
+
if (editor)
|
|
1730
|
+
editor.lock(t('msg.input_locked'));
|
|
1611
1731
|
try {
|
|
1612
1732
|
console.log(theme.system(t('msg.opening_chat', { id })));
|
|
1613
1733
|
await browser.openChat(id);
|
|
@@ -1622,12 +1742,70 @@ async function main() {
|
|
|
1622
1742
|
catch (e) {
|
|
1623
1743
|
console.error(theme.error(t('msg.open_chat_error', { v: '' })), e.message);
|
|
1624
1744
|
}
|
|
1745
|
+
finally {
|
|
1746
|
+
if (editor)
|
|
1747
|
+
editor.unlock();
|
|
1748
|
+
}
|
|
1625
1749
|
continue;
|
|
1626
1750
|
}
|
|
1627
1751
|
if (lower === '/pwd') {
|
|
1628
1752
|
console.log(theme.system(currentWorkdir));
|
|
1629
1753
|
continue;
|
|
1630
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
|
+
}
|
|
1631
1809
|
if (lower === '/reload') {
|
|
1632
1810
|
console.log(theme.system(t('msg.reload_start')));
|
|
1633
1811
|
try {
|
|
@@ -1644,6 +1822,10 @@ async function main() {
|
|
|
1644
1822
|
catch (e) {
|
|
1645
1823
|
console.error(theme.error(t('msg.reload_error')), e.message);
|
|
1646
1824
|
}
|
|
1825
|
+
await autoReload();
|
|
1826
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
1827
|
+
if (editor)
|
|
1828
|
+
editor.setCommands(buildSlashCommands());
|
|
1647
1829
|
continue;
|
|
1648
1830
|
}
|
|
1649
1831
|
if (lower === '/status') {
|
|
@@ -1754,6 +1936,9 @@ async function main() {
|
|
|
1754
1936
|
currentWorkdir = newDir;
|
|
1755
1937
|
freshChatNext = true;
|
|
1756
1938
|
sendSystemPromptNext = true;
|
|
1939
|
+
await refreshDynamicCommands(currentWorkdir);
|
|
1940
|
+
if (editor)
|
|
1941
|
+
editor.setCommands(buildSlashCommands());
|
|
1757
1942
|
console.log(theme.system(t('cd.changed', { v: newDir })));
|
|
1758
1943
|
}
|
|
1759
1944
|
catch (e) {
|
|
@@ -1761,19 +1946,29 @@ async function main() {
|
|
|
1761
1946
|
}
|
|
1762
1947
|
continue;
|
|
1763
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;
|
|
1764
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) {
|
|
1765
1959
|
console.error(theme.error(t('msg.unknown_cmd', { v: trimmed })));
|
|
1766
1960
|
continue;
|
|
1767
1961
|
}
|
|
1768
1962
|
// ---- Regular task (including in review mode) ----
|
|
1963
|
+
const taskText = expandedTask !== null ? expandedTask : trimmed;
|
|
1769
1964
|
// Dev mode: pick up fresh logic modules before the task.
|
|
1770
1965
|
await autoReload();
|
|
1771
|
-
transcript.log('user_task', { task:
|
|
1966
|
+
transcript.log('user_task', { task: taskText, workdir: currentWorkdir });
|
|
1772
1967
|
const tools = mod.createTools(currentWorkdir, { undo });
|
|
1773
1968
|
if (editor)
|
|
1774
1969
|
editor.busy = true;
|
|
1775
1970
|
try {
|
|
1776
|
-
await runTask(browser, tools,
|
|
1971
|
+
await runTask(browser, tools, taskText, currentWorkdir, {
|
|
1777
1972
|
transcript,
|
|
1778
1973
|
freshChat: freshChatNext,
|
|
1779
1974
|
sendSystemPrompt: sendSystemPromptNext,
|
package/dist/input.js
CHANGED
|
@@ -207,6 +207,11 @@ export class LineEditor {
|
|
|
207
207
|
// Clipboard callback: tries to read an image from the OS clipboard when the
|
|
208
208
|
// terminal itself sends no usable data (Ctrl+V, right-click, empty paste).
|
|
209
209
|
onClipboard;
|
|
210
|
+
// When locked, the editor ignores text input and Enter (submit). Used while
|
|
211
|
+
// a long operation runs (chat resume/open, /new, self-review): otherwise the
|
|
212
|
+
// user could type and send a message mid-operation, and it would be queued
|
|
213
|
+
// and sent right after the operation, breaking the restored session.
|
|
214
|
+
locked;
|
|
210
215
|
constructor({ prompt = '> ', commands = [] } = {}) {
|
|
211
216
|
this.promptStr = prompt;
|
|
212
217
|
this.buf = '';
|
|
@@ -236,6 +241,7 @@ export class LineEditor {
|
|
|
236
241
|
this._tmpDir = '';
|
|
237
242
|
this.onAttach = null;
|
|
238
243
|
this.onClipboard = null;
|
|
244
|
+
this.locked = false;
|
|
239
245
|
}
|
|
240
246
|
// Read the OS clipboard for an image and insert its marker. Used when the
|
|
241
247
|
// terminal sends no usable paste data (Ctrl+V / right-click / empty paste).
|
|
@@ -333,6 +339,19 @@ export class LineEditor {
|
|
|
333
339
|
this._writeBlock();
|
|
334
340
|
this._render();
|
|
335
341
|
}
|
|
342
|
+
// Lock input while a long operation runs (chat resume/open, /new,
|
|
343
|
+
// self-review). The line stays visible but text input and Enter are ignored,
|
|
344
|
+
// so a message typed mid-operation is not queued and sent afterwards,
|
|
345
|
+
// breaking the restored session. Ctrl+C/Ctrl+D still work so the user can
|
|
346
|
+
// abort.
|
|
347
|
+
lock(status) {
|
|
348
|
+
this.locked = true;
|
|
349
|
+
if (status)
|
|
350
|
+
this.setStatus(theme.dim(status));
|
|
351
|
+
}
|
|
352
|
+
unlock() {
|
|
353
|
+
this.locked = false;
|
|
354
|
+
}
|
|
336
355
|
setPrompt(str) {
|
|
337
356
|
this.promptStr = str;
|
|
338
357
|
this._render();
|
|
@@ -481,6 +500,10 @@ export class LineEditor {
|
|
|
481
500
|
}
|
|
482
501
|
// ---------- input ----------
|
|
483
502
|
_submit() {
|
|
503
|
+
if (this.locked) {
|
|
504
|
+
this._render();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
484
507
|
const display = this.buf;
|
|
485
508
|
if (!display.trim()) {
|
|
486
509
|
this._render();
|
|
@@ -716,6 +739,25 @@ export class LineEditor {
|
|
|
716
739
|
}
|
|
717
740
|
_handle(data) {
|
|
718
741
|
let s = data.toString('utf-8');
|
|
742
|
+
// While locked (a long operation is running), swallow all input except
|
|
743
|
+
// Ctrl+C / Ctrl+D — so the user can still abort, but cannot type a message
|
|
744
|
+
// that would be queued and sent after the operation.
|
|
745
|
+
if (this.locked) {
|
|
746
|
+
if (s.length === 1) {
|
|
747
|
+
const c = s.charCodeAt(0);
|
|
748
|
+
if (c === 3) {
|
|
749
|
+
if (this.onCtrlC)
|
|
750
|
+
this.onCtrlC();
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (c === 4) {
|
|
754
|
+
if (this.onCtrlC)
|
|
755
|
+
this.onCtrlC();
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
719
761
|
if (!this._inPaste && s === ESC) {
|
|
720
762
|
if (this.onEscape)
|
|
721
763
|
this.onEscape();
|
package/dist/system-prompt.js
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
import { translate } from './i18n.js';
|
|
2
|
-
|
|
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):
|