zames_pro 2.6.0 → 2.8.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 +21 -0
- package/dist/agent-loop.js +141 -83
- package/dist/attachments.js +267 -0
- package/dist/browser.js +139 -50
- package/dist/config-menu.js +25 -25
- package/dist/config.js +5 -5
- package/dist/gitTools.js +4 -4
- package/dist/i18n.js +31 -7
- package/dist/index.js +295 -168
- package/dist/input.js +237 -83
- package/dist/markdown.js +1 -1
- package/dist/net-capture.js +20 -20
- package/dist/self-review.js +16 -16
- package/dist/sessions.js +11 -11
- package/dist/spinner.js +20 -16
- package/dist/system-prompt.js +29 -2
- package/dist/theme.js +26 -26
- package/dist/tools.js +9 -9
- package/dist/transcript.js +2 -2
- package/dist/types.js +2 -2
- package/dist/web.js +16 -16
- package/dist/xml-toolcall.js +89 -14
- package/package.json +1 -1
- package/scripts/postinstall.mjs +78 -51
package/dist/net-capture.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import fs from 'fs/promises';
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
//
|
|
4
|
+
// Extraction of the RAW model answer text from DeepSeek's network data.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// Why: the answer is read from the RENDERED DOM, and DeepSeek's rendering
|
|
7
|
+
// distorts the answer — it turns dollar formulas into LaTeX (the dollar sign
|
|
8
|
+
// is lost), normalizes newlines, and auto-links. Because of this a tool-call
|
|
9
|
+
// with template strings and escaped newlines in its arguments reached the
|
|
10
|
+
// tools distorted. Here we extract the original text from the network
|
|
11
|
+
// response body.
|
|
12
12
|
//
|
|
13
13
|
// Format DeepSeek (SSE, chat.deepseek.com/api/v0/chat/completion):
|
|
14
14
|
// * startovy fragment otveta lezhit v data-chanke s uzlom v.response
|
|
@@ -36,11 +36,11 @@ function parseDataLines(body) {
|
|
|
36
36
|
}
|
|
37
37
|
return out;
|
|
38
38
|
}
|
|
39
|
-
//
|
|
40
|
-
// * OpenAI
|
|
41
|
-
//
|
|
42
|
-
// * DeepSeek chat.deepseek.com:
|
|
43
|
-
//
|
|
39
|
+
// Assembles the answer text from the SSE stream. Supports two formats:
|
|
40
|
+
// * OpenAI-compatible: choices[].delta.content (reasoning_content
|
|
41
|
+
// is ignored — that's reasoning, not the answer);
|
|
42
|
+
// * DeepSeek chat.deepseek.com: the initial v.response.fragments[] and
|
|
43
|
+
// incremental APPEND chunks response/fragments/-1/content.
|
|
44
44
|
export function extractFromSse(body) {
|
|
45
45
|
let out = '';
|
|
46
46
|
for (const obj of parseDataLines(body)) {
|
|
@@ -65,8 +65,8 @@ export function extractFromSse(body) {
|
|
|
65
65
|
}
|
|
66
66
|
continue;
|
|
67
67
|
}
|
|
68
|
-
//
|
|
69
|
-
//
|
|
68
|
+
// Incremental APPEND: p='response/fragments/-1/content' -> v=string;
|
|
69
|
+
// subsequent chunks come with a single v field.
|
|
70
70
|
if (o.p === 'response/fragments/-1/content' && typeof o.v === 'string') {
|
|
71
71
|
out += o.v;
|
|
72
72
|
continue;
|
|
@@ -87,9 +87,9 @@ export function extractFromJson(body) {
|
|
|
87
87
|
}
|
|
88
88
|
return pickAnswerText(obj);
|
|
89
89
|
}
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
90
|
+
// Extracts the final answer text from a node, WITHOUT mixing it with reasoning.
|
|
91
|
+
// Order: choices/messages -> delta/message -> content; reasoning_content is
|
|
92
|
+
// deliberately not taken — that's the model's reasoning, not the answer.
|
|
93
93
|
function pickAnswerText(node) {
|
|
94
94
|
if (node == null)
|
|
95
95
|
return '';
|
|
@@ -124,15 +124,15 @@ function pickAnswerText(node) {
|
|
|
124
124
|
}
|
|
125
125
|
return '';
|
|
126
126
|
}
|
|
127
|
-
//
|
|
127
|
+
// A universal attempt: first SSE, then regular JSON.
|
|
128
128
|
export function extractAnswer(body) {
|
|
129
129
|
const sse = extractFromSse(body);
|
|
130
130
|
if (sse)
|
|
131
131
|
return sse;
|
|
132
132
|
return extractFromJson(body);
|
|
133
133
|
}
|
|
134
|
-
//
|
|
135
|
-
//
|
|
134
|
+
// Saves the DeepSeek network response body to disk for post-mortem analysis.
|
|
135
|
+
// The files live in ~/.zames/net-log — from them the real answer format is visible.
|
|
136
136
|
export async function dumpNetBody(url, body) {
|
|
137
137
|
try {
|
|
138
138
|
const dir = path.join(os.homedir(), '.zames', 'net-log');
|
package/dist/self-review.js
CHANGED
|
@@ -4,10 +4,10 @@ import { theme } from './theme.js';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { ZAMES_HOME } from './config.js';
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
//
|
|
8
|
-
// __dirname
|
|
9
|
-
//
|
|
10
|
-
// (
|
|
7
|
+
// The sources for self-review are .ts files. After the build (tsc → dist/)
|
|
8
|
+
// __dirname points to dist, where the .js files live. So we look for the
|
|
9
|
+
// source directory: either nearby (running from src/ via tsx), or in ../src
|
|
10
|
+
// (running the built dist/ from the repo root).
|
|
11
11
|
function resolveSrcDir() {
|
|
12
12
|
const candidates = [__dirname, path.join(__dirname, '..', 'src')];
|
|
13
13
|
for (const dir of candidates) {
|
|
@@ -22,9 +22,9 @@ function resolveSrcDir() {
|
|
|
22
22
|
}
|
|
23
23
|
const SRC_DIR = resolveSrcDir();
|
|
24
24
|
const SNAP_ROOT = path.join(ZAMES_HOME, 'snapshots');
|
|
25
|
-
//
|
|
26
|
-
// self-review
|
|
27
|
-
//
|
|
25
|
+
// Dynamic import of the logic with a timestamp — so that on /reload (or
|
|
26
|
+
// auto-reload) self-review uses FRESH tools/agent-loop, not the ones cached on
|
|
27
|
+
// first load. Otherwise reload wouldn't reach self-review's dependencies.
|
|
28
28
|
async function loadFresh() {
|
|
29
29
|
const stamp = Date.now();
|
|
30
30
|
const toolsUrl = new URL('./tools.js', import.meta.url);
|
|
@@ -66,7 +66,7 @@ export async function selfReview({ browser, focus, transcript }) {
|
|
|
66
66
|
`Проверь, что src/ не пуст и ты запускаешь агента из корня проекта.\n`));
|
|
67
67
|
throw new Error('SRC_DIR пуст — нечего ревьюить');
|
|
68
68
|
}
|
|
69
|
-
//
|
|
69
|
+
// Copy package.json for context — so the agent sees the dependencies
|
|
70
70
|
try {
|
|
71
71
|
const pkg = await fs.readFile(path.resolve(SRC_DIR, '..', 'package.json'), 'utf-8');
|
|
72
72
|
await fs.writeFile(path.join(snapDir, 'package.json'), pkg, 'utf-8');
|
|
@@ -104,10 +104,10 @@ export async function selfReview({ browser, focus, transcript }) {
|
|
|
104
104
|
console.log();
|
|
105
105
|
},
|
|
106
106
|
});
|
|
107
|
-
//
|
|
107
|
+
// Save the report
|
|
108
108
|
const reportPath = path.join(snapDir, '_report.md');
|
|
109
109
|
await fs.writeFile(reportPath, finalMessage || '(пусто)', 'utf-8');
|
|
110
|
-
//
|
|
110
|
+
// Compute what changed
|
|
111
111
|
const changed = await diffFiles(SRC_DIR, snapDir);
|
|
112
112
|
console.log(theme.system('─'.repeat(60)));
|
|
113
113
|
console.log(theme.system(`Отчёт: ${reportPath}`));
|
|
@@ -159,7 +159,7 @@ export async function selfApply({ name }) {
|
|
|
159
159
|
const snapStat = await fs.stat(snapDir).catch(() => null);
|
|
160
160
|
if (!snapStat)
|
|
161
161
|
throw new Error(`Снапшот не найден: ${snapDir}`);
|
|
162
|
-
//
|
|
162
|
+
// Back up the current src before overwriting
|
|
163
163
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
164
164
|
const backupDir = path.join(SNAP_ROOT, `backup-before-apply-${stamp}`);
|
|
165
165
|
await copyDirJsFiles(SRC_DIR, backupDir);
|
|
@@ -264,16 +264,16 @@ ${focusLine}
|
|
|
264
264
|
|
|
265
265
|
When done, respond with a markdown report as the message:
|
|
266
266
|
|
|
267
|
-
##
|
|
267
|
+
## Found
|
|
268
268
|
- (bullet list of issues you found, with file:line if possible)
|
|
269
269
|
|
|
270
|
-
##
|
|
271
|
-
- (bullet list: file —
|
|
270
|
+
## Fixed
|
|
271
|
+
- (bullet list: file — what exactly you changed)
|
|
272
272
|
|
|
273
|
-
##
|
|
273
|
+
## Not fixed (deliberately)
|
|
274
274
|
- (bullet list: what you left alone and why)
|
|
275
275
|
|
|
276
|
-
##
|
|
276
|
+
## Verification
|
|
277
277
|
- (bullet list: what you verified, e.g. \`node --check\` results)
|
|
278
278
|
|
|
279
279
|
Be honest and specific. If the code is fine in some area, say so.`;
|
package/dist/sessions.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import os from 'os';
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// <id>.json
|
|
7
|
-
//
|
|
4
|
+
// We store sessions (DeepSeek chats) in a separate folder so they aren't lost
|
|
5
|
+
// after a process restart. Each session is a separate JSON file
|
|
6
|
+
// <id>.json in ~/.zames/.sessions. last.json points to the last
|
|
7
|
+
// opened session for a specific working directory.
|
|
8
8
|
const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions');
|
|
9
9
|
const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json');
|
|
10
10
|
function ensureDir() {
|
|
@@ -13,8 +13,8 @@ function ensureDir() {
|
|
|
13
13
|
function safeName(id) {
|
|
14
14
|
return String(id).replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
15
15
|
}
|
|
16
|
-
//
|
|
17
|
-
//
|
|
16
|
+
// Save/update a session. workdir is needed so that when launched from the same
|
|
17
|
+
// project we restore exactly its last chat.
|
|
18
18
|
export function saveSession({ id, title = '', workdir = '', }) {
|
|
19
19
|
if (!id)
|
|
20
20
|
return null;
|
|
@@ -63,16 +63,16 @@ function writeIndex({ id, workdir }) {
|
|
|
63
63
|
}
|
|
64
64
|
catch { }
|
|
65
65
|
}
|
|
66
|
-
//
|
|
67
|
-
//
|
|
66
|
+
// The last session for the working directory. If there is none —
|
|
67
|
+
// we return the last session overall (useful when launched from a new place).
|
|
68
68
|
export function loadLastSession(workdir = '') {
|
|
69
69
|
try {
|
|
70
70
|
const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'));
|
|
71
71
|
const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {};
|
|
72
72
|
const candidates = [byWorkdir[workdir], index.last].filter((x) => Boolean(x));
|
|
73
73
|
for (const id of candidates) {
|
|
74
|
-
//
|
|
75
|
-
//
|
|
74
|
+
// The session file may have been deleted — the index is then stale and
|
|
75
|
+
// the session cannot be restored. We try the next candidate.
|
|
76
76
|
const s = readSession(id);
|
|
77
77
|
if (s)
|
|
78
78
|
return s;
|
|
@@ -94,7 +94,7 @@ export function readSession(id) {
|
|
|
94
94
|
return null;
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
//
|
|
97
|
+
// List all sessions, newest first.
|
|
98
98
|
export function listSessions() {
|
|
99
99
|
try {
|
|
100
100
|
ensureDir();
|
package/dist/spinner.js
CHANGED
|
@@ -2,8 +2,8 @@ import ora from 'ora';
|
|
|
2
2
|
import { theme } from './theme.js';
|
|
3
3
|
import { renderMarkdown } from './markdown.js';
|
|
4
4
|
import { translate } from './i18n.js';
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// Phrases for the "agent is working" spinner. Chosen in random order.
|
|
6
|
+
// The text comes from i18n via the key 'spinner.phrases' (strings separated by "|").
|
|
7
7
|
export function randomThinkingPhrase(locale = 'ru') {
|
|
8
8
|
const raw = translate(locale)('spinner.phrases');
|
|
9
9
|
const phrases = raw.split('|').filter((s) => s.trim().length);
|
|
@@ -11,7 +11,7 @@ export function randomThinkingPhrase(locale = 'ru') {
|
|
|
11
11
|
return '';
|
|
12
12
|
return phrases[Math.floor(Math.random() * phrases.length)];
|
|
13
13
|
}
|
|
14
|
-
//
|
|
14
|
+
// Strip the trailing ellipsis from the phrase — dots are animated separately.
|
|
15
15
|
export function stripEllipsis(phrase) {
|
|
16
16
|
return phrase.replace(/[.…]+\s*$/, '');
|
|
17
17
|
}
|
|
@@ -20,8 +20,8 @@ export function createSpinner(locale = 'ru') {
|
|
|
20
20
|
let dotTimer = null;
|
|
21
21
|
let dotPhase = 0;
|
|
22
22
|
let pending = null;
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// Dot animation: start from an empty string (0 dots), then grow.
|
|
24
|
+
// We align the width to the maximum (3) so the hint doesn't shift.
|
|
25
25
|
const DOTS = ['', '.', '..', '...'];
|
|
26
26
|
const DOTS_PAD = ' ';
|
|
27
27
|
const start = (text) => {
|
|
@@ -38,14 +38,14 @@ export function createSpinner(locale = 'ru') {
|
|
|
38
38
|
if (spinner)
|
|
39
39
|
spinner.stop();
|
|
40
40
|
};
|
|
41
|
-
//
|
|
42
|
-
//
|
|
41
|
+
// Hint in the status line: while the agent works the terminal is live,
|
|
42
|
+
// so you can type the next message. Without it this is not obvious.
|
|
43
43
|
const HINT = theme.dim(' · ' + translate(locale)('spinner.hint'));
|
|
44
|
-
//
|
|
44
|
+
// Start the animated status: brown text + "running" dots.
|
|
45
45
|
const startThinking = () => {
|
|
46
46
|
const base = theme.brown(stripEllipsis(randomThinkingPhrase(locale)));
|
|
47
|
-
//
|
|
48
|
-
//
|
|
47
|
+
// The dots are the same color as the base and of fixed width — otherwise
|
|
48
|
+
// the hint on the right "jumps" when the animation phase changes.
|
|
49
49
|
const dots = (n) => theme.brown(DOTS[n] + DOTS_PAD.slice(DOTS[n].length));
|
|
50
50
|
start(base + dots(0) + HINT);
|
|
51
51
|
dotPhase = 0;
|
|
@@ -60,8 +60,8 @@ export function createSpinner(locale = 'ru') {
|
|
|
60
60
|
if (dotTimer.unref)
|
|
61
61
|
dotTimer.unref();
|
|
62
62
|
};
|
|
63
|
-
//
|
|
64
|
-
//
|
|
63
|
+
// Show the typed but not yet sent text instead of the spinner.
|
|
64
|
+
// Lets you type a message right while the agent is working.
|
|
65
65
|
const showPending = () => {
|
|
66
66
|
if (dotTimer) {
|
|
67
67
|
clearInterval(dotTimer);
|
|
@@ -80,8 +80,8 @@ export function createSpinner(locale = 'ru') {
|
|
|
80
80
|
}
|
|
81
81
|
startThinking();
|
|
82
82
|
},
|
|
83
|
-
//
|
|
84
|
-
//
|
|
83
|
+
// The text the user types while the agent is working.
|
|
84
|
+
// Empty string / null — restore the regular spinner.
|
|
85
85
|
setPending: (text) => {
|
|
86
86
|
pending = text && String(text).length ? String(text) : null;
|
|
87
87
|
if (pending)
|
|
@@ -104,12 +104,16 @@ export function createSpinner(locale = 'ru') {
|
|
|
104
104
|
stop();
|
|
105
105
|
const NL = String.fromCharCode(10);
|
|
106
106
|
const rendered = renderMarkdown(msg);
|
|
107
|
-
//
|
|
108
|
-
//
|
|
107
|
+
// Model answer marker: helps visually separate it from the user's input
|
|
108
|
+
// (which is highlighted by the prompt with a golden arrow).
|
|
109
109
|
console.log(NL + theme.assistant('● Ответ') + NL);
|
|
110
110
|
console.log(rendered);
|
|
111
111
|
console.log(theme.dim('─'.repeat(60)) + NL);
|
|
112
112
|
},
|
|
113
|
+
warning: (msg) => {
|
|
114
|
+
stop();
|
|
115
|
+
console.log(String.fromCharCode(10) + theme.warn('⚠ ' + msg));
|
|
116
|
+
},
|
|
113
117
|
stop,
|
|
114
118
|
succeed: () => { },
|
|
115
119
|
fail: () => { },
|
package/dist/system-prompt.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { translate } from './i18n.js';
|
|
2
|
-
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', }) {
|
|
2
|
+
export function buildSystemPrompt({ workdir, tools, gitContext = null, locale = 'ru', attachments = [], }) {
|
|
3
3
|
const t = translate(locale);
|
|
4
4
|
const toolDescriptions = tools
|
|
5
5
|
.map((t2) => `### ${t2.name}\n${t2.description}\nПараметры: ${JSON.stringify(t2.parameters)}`)
|
|
@@ -7,6 +7,10 @@ export function buildSystemPrompt({ workdir, tools, gitContext = null, locale =
|
|
|
7
7
|
const gitSection = gitContext
|
|
8
8
|
? `\n## Git context\n\n${gitContext}\n`
|
|
9
9
|
: '\n## Git context\n\nNot a git repository (or git is not installed).\n';
|
|
10
|
+
const attachSection = attachments.length
|
|
11
|
+
? `\n## Attachments\n\nThe user attached files to this task: ${attachments.join(', ')}.\n` +
|
|
12
|
+
`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
|
+
: '';
|
|
10
14
|
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.
|
|
11
15
|
|
|
12
16
|
You work in the directory: ${workdir}
|
|
@@ -46,7 +50,7 @@ operator will not read it, so never use plain text.\n\nNO PROSE AROUND TOOL CALL
|
|
|
46
50
|
You have access to the following tools:
|
|
47
51
|
|
|
48
52
|
${toolDescriptions}
|
|
49
|
-
${gitSection}
|
|
53
|
+
${gitSection}${attachSection}
|
|
50
54
|
## How to use tools
|
|
51
55
|
|
|
52
56
|
To call ONE tool, respond with ONLY a JSON object (no markdown fences, no extra text):
|
|
@@ -65,6 +69,29 @@ To give a final answer to the user, respond with:
|
|
|
65
69
|
|
|
66
70
|
{"tool": "respond", "args": {"message": "your final answer here"}}
|
|
67
71
|
|
|
72
|
+
## TOOL CALL FORMAT - EXACT REQUIREMENTS (read carefully)
|
|
73
|
+
|
|
74
|
+
The parser is strict about the SHAPE of your call. These mistakes have caused
|
|
75
|
+
the agent to silently stall (the call was not recognized, so the task ended):
|
|
76
|
+
|
|
77
|
+
1. ALWAYS wrap the call in curly braces { ... }. Never emit tool-colon-quote
|
|
78
|
+
without the leading brace. Never drop the very first character.
|
|
79
|
+
2. ALWAYS put DOUBLE quotes around keys and string values, e.g.
|
|
80
|
+
{"tool": "Read", "args": {"path": "a.ts"}}.
|
|
81
|
+
NEVER use single quotes; that is not valid JSON.
|
|
82
|
+
3. NEVER wrap the call in markdown fences or in XML/DSML tags
|
|
83
|
+
(invoke, parameter, tool_calls, DSML variants). Plain JSON only.
|
|
84
|
+
4. NEVER put prose before or after the JSON in the same response. Not even
|
|
85
|
+
a short lead-in. The whole response is the JSON.
|
|
86
|
+
5. Do NOT truncate long calls. If a Bash command, file content, or Edit is
|
|
87
|
+
large, SPLIT it: run several smaller Bash commands, or write the file in
|
|
88
|
+
parts (Write then Edit). A cut-off JSON never parses and stalls the agent.
|
|
89
|
+
6. One object per call; to call several independent tools at once, use a JSON
|
|
90
|
+
ARRAY of objects, still no prose around it.
|
|
91
|
+
|
|
92
|
+
If you catch yourself about to emit anything other than a bare JSON object or
|
|
93
|
+
array, stop and reformat it first. A malformed call is worse than a slow one.
|
|
94
|
+
|
|
68
95
|
## Git
|
|
69
96
|
|
|
70
97
|
- You CAN commit and push using GitAdd / GitCommit / GitPush. Prefer these over raw \`git\` through Bash.
|
package/dist/theme.js
CHANGED
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
5
|
-
//
|
|
2
|
+
// A calm palette for a dark terminal.
|
|
3
|
+
// Muted, slightly "faded" tones without high saturation
|
|
4
|
+
// (in the spirit of Tokyo Night / Nord). The goal is comfortable reading of
|
|
5
|
+
// long answers, not maximum contrast.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
// - user —
|
|
9
|
-
// - prompt —
|
|
10
|
-
// - dir —
|
|
11
|
-
// - assistant —
|
|
12
|
-
// - tool —
|
|
13
|
-
// - toolResult —
|
|
14
|
-
// - system —
|
|
15
|
-
// - warn —
|
|
16
|
-
// - error —
|
|
7
|
+
// Roles:
|
|
8
|
+
// - user — soft lavender-gray (service messages)
|
|
9
|
+
// - prompt — warm gold (input prompt, noticeable)
|
|
10
|
+
// - dir — light blue (working directory name)
|
|
11
|
+
// - assistant — warm sand (model answers)
|
|
12
|
+
// - tool — muted amber
|
|
13
|
+
// - toolResult — gray-blue
|
|
14
|
+
// - system — neutral gray
|
|
15
|
+
// - warn — calm ochre
|
|
16
|
+
// - error — muted terracotta
|
|
17
17
|
export const theme = {
|
|
18
|
-
user: chalk.hex('#b4b8d0'), //
|
|
19
|
-
prompt: chalk.hex('#c8b06a').bold, //
|
|
20
|
-
dir: chalk.hex('#7fc4f0'), //
|
|
21
|
-
assistant: chalk.hex('#cfc9b0'), //
|
|
22
|
-
tool: chalk.hex('#c6a97e'), //
|
|
23
|
-
toolResult: chalk.hex('#8a9bb5'), //
|
|
24
|
-
system: chalk.hex('#808896'), //
|
|
25
|
-
dim: chalk.hex('#5b616e'), //
|
|
26
|
-
warn: chalk.hex('#c9a86a'), //
|
|
27
|
-
error: chalk.hex('#c98a80'), //
|
|
28
|
-
success: chalk.hex('#a9c08c'), //
|
|
29
|
-
brown: chalk.hex('#a1723f'), //
|
|
18
|
+
user: chalk.hex('#b4b8d0'), // soft lavender-gray
|
|
19
|
+
prompt: chalk.hex('#c8b06a').bold, // input prompt — warm gold, noticeable
|
|
20
|
+
dir: chalk.hex('#7fc4f0'), // working directory name — noticeably blue
|
|
21
|
+
assistant: chalk.hex('#cfc9b0'), // warm sand
|
|
22
|
+
tool: chalk.hex('#c6a97e'), // muted amber
|
|
23
|
+
toolResult: chalk.hex('#8a9bb5'), // gray-blue
|
|
24
|
+
system: chalk.hex('#808896'), // neutral gray
|
|
25
|
+
dim: chalk.hex('#5b616e'), // dark gray
|
|
26
|
+
warn: chalk.hex('#c9a86a'), // calm ochre
|
|
27
|
+
error: chalk.hex('#c98a80'), // muted terracotta
|
|
28
|
+
success: chalk.hex('#a9c08c'), // soft sage
|
|
29
|
+
brown: chalk.hex('#a1723f'), // brown (status/spinner)
|
|
30
30
|
bold: chalk.bold,
|
|
31
31
|
};
|
|
32
32
|
export default theme;
|
package/dist/tools.js
CHANGED
|
@@ -7,16 +7,16 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
7
7
|
const root = path.resolve(workdir);
|
|
8
8
|
const safe = (p) => {
|
|
9
9
|
const resolved = path.resolve(root, p);
|
|
10
|
-
// startsWith(root)
|
|
11
|
-
// (C:\work\proj vs C:\work\proj-old).
|
|
10
|
+
// startsWith(root) would let through sibling paths with a common prefix
|
|
11
|
+
// (C:\work\proj vs C:\work\proj-old). We compute via relative().
|
|
12
12
|
const rel = path.relative(root, resolved);
|
|
13
13
|
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
14
14
|
throw new Error(`Доступ за пределы рабочей директории: ${p}`);
|
|
15
15
|
}
|
|
16
16
|
return resolved;
|
|
17
17
|
};
|
|
18
|
-
// Sandbox (
|
|
19
|
-
//
|
|
18
|
+
// Sandbox (option A): don't let the command go above root.
|
|
19
|
+
// This is a protective barrier, not full OS isolation.
|
|
20
20
|
const assertCommandInsideRoot = (command) => {
|
|
21
21
|
const cmd = String(command || '');
|
|
22
22
|
const cdRe = /(?:^|[;&|]|\s)(?:cd|pushd)\s+([^;&|]+)/gi;
|
|
@@ -74,10 +74,10 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
74
74
|
resolve(parts.join('\n'));
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
77
|
+
// Extracts text from content/content_base64. base64 is needed because the
|
|
78
|
+
// channel that delivers the model's answer may corrupt characters ($,
|
|
79
|
+
// backslashes, newlines). base64 consists only of [A-Za-z0-9+/=] and is not
|
|
80
|
+
// subject to corruption.
|
|
81
81
|
const decodeContent = (content, contentBase64) => {
|
|
82
82
|
if (typeof contentBase64 === 'string' && contentBase64.length) {
|
|
83
83
|
return Buffer.from(contentBase64, 'base64').toString('utf-8');
|
|
@@ -172,7 +172,7 @@ export function createTools(workdir, { undo } = {}) {
|
|
|
172
172
|
const { glob } = await import('fs/promises');
|
|
173
173
|
const results = [];
|
|
174
174
|
for await (const f of glob(String(pattern), { cwd: workdir })) {
|
|
175
|
-
// Sandbox:
|
|
175
|
+
// Sandbox: ignore anything that goes outside root.
|
|
176
176
|
const abs = path.resolve(workdir, f);
|
|
177
177
|
const rel = path.relative(root, abs);
|
|
178
178
|
if (rel.startsWith('..') || path.isAbsolute(rel))
|
package/dist/transcript.js
CHANGED
|
@@ -17,8 +17,8 @@ export class Transcript {
|
|
|
17
17
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
18
18
|
this.file = path.join(dir, `${sessionName}-${stamp}.jsonl`);
|
|
19
19
|
this.stream = fs.createWriteStream(this.file, { flags: 'a' });
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// Write errors (disk full, file deleted, etc.) arrive as an 'error'
|
|
21
|
+
// event; without a listener this is an uncaught exception.
|
|
22
22
|
this.stream.on('error', (e) => {
|
|
23
23
|
console.error(`transcript: ошибка записи: ${e.message}`);
|
|
24
24
|
this.enabled = false;
|
package/dist/types.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Shared contract types for the whole agent.
|
|
2
|
+
// Types only here — no runtime code, no side effects.
|
|
3
3
|
export {};
|
package/dist/web.js
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { chromium } from 'playwright';
|
|
2
2
|
const DEFAULT_TIMEOUT = 20_000;
|
|
3
3
|
const MAX_TEXT = 12_000;
|
|
4
|
-
//
|
|
4
|
+
// Removes scripts, styles, and turns HTML into readable text.
|
|
5
5
|
function htmlToText(html) {
|
|
6
|
-
//
|
|
6
|
+
// Remove blocks we don't need
|
|
7
7
|
let s = html
|
|
8
8
|
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
9
9
|
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
|
10
10
|
.replace(/<noscript[\s\S]*?<\/noscript>/gi, '')
|
|
11
11
|
.replace(/<svg[\s\S]*?<\/svg>/gi, '')
|
|
12
12
|
.replace(/<!--[\s\S]*?-->/g, '');
|
|
13
|
-
//
|
|
13
|
+
// Links: <a href="URL">text</a> → text (URL)
|
|
14
14
|
s = s.replace(/<a\b[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi, (_, href, text) => {
|
|
15
15
|
const t = text.replace(/<[^>]+>/g, '').trim();
|
|
16
16
|
return t ? `${t} (${href})` : href;
|
|
17
17
|
});
|
|
18
|
-
//
|
|
18
|
+
// Headings, paragraphs, BR → newlines
|
|
19
19
|
s = s
|
|
20
20
|
.replace(/<\/(h[1-6]|p|div|li|tr|section|article)>/gi, '\n')
|
|
21
21
|
.replace(/<br\s*\/?>/gi, '\n')
|
|
22
22
|
.replace(/<li[^>]*>/gi, '- ')
|
|
23
23
|
.replace(/<[^>]+>/g, '');
|
|
24
|
-
// HTML entities —
|
|
24
|
+
// HTML entities — the basic ones
|
|
25
25
|
s = s
|
|
26
26
|
.replace(/ /g, ' ')
|
|
27
27
|
.replace(/&/g, '&')
|
|
@@ -34,7 +34,7 @@ function htmlToText(html) {
|
|
|
34
34
|
.replace(/…/g, '…')
|
|
35
35
|
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
|
36
36
|
.replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)));
|
|
37
|
-
//
|
|
37
|
+
// Collapse blank lines
|
|
38
38
|
s = s
|
|
39
39
|
.split('\n')
|
|
40
40
|
.map((l) => l.replace(/[ \t]+/g, ' ').trim())
|
|
@@ -42,7 +42,7 @@ function htmlToText(html) {
|
|
|
42
42
|
.join('\n');
|
|
43
43
|
return s;
|
|
44
44
|
}
|
|
45
|
-
// fetch
|
|
45
|
+
// fetch with redirects and a timeout
|
|
46
46
|
async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {} } = {}) {
|
|
47
47
|
const ctrl = new AbortController();
|
|
48
48
|
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
@@ -64,8 +64,8 @@ async function httpFetch(url, { timeout = DEFAULT_TIMEOUT, headers = {} } = {})
|
|
|
64
64
|
clearTimeout(t);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
//
|
|
68
|
-
//
|
|
67
|
+
// A separate headless browser for JS pages.
|
|
68
|
+
// Lazy initialization so we don't waste resources.
|
|
69
69
|
let _headless = null;
|
|
70
70
|
async function getHeadless() {
|
|
71
71
|
if (_headless)
|
|
@@ -96,7 +96,7 @@ async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
|
|
|
96
96
|
waitUntil: 'domcontentloaded',
|
|
97
97
|
timeout,
|
|
98
98
|
});
|
|
99
|
-
//
|
|
99
|
+
// Give JS a little time to finish rendering
|
|
100
100
|
await page.waitForTimeout(1500);
|
|
101
101
|
const html = await page.content();
|
|
102
102
|
const status = resp ? resp.status() : 0;
|
|
@@ -107,7 +107,7 @@ async function renderWithHeadless(url, { timeout = DEFAULT_TIMEOUT } = {}) {
|
|
|
107
107
|
await ctx.close();
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
-
// ----------
|
|
110
|
+
// ---------- tools ----------
|
|
111
111
|
export function createWebTools() {
|
|
112
112
|
return [
|
|
113
113
|
{
|
|
@@ -132,14 +132,14 @@ export function createWebTools() {
|
|
|
132
132
|
return formatResult(r.status, r.url, text, limit);
|
|
133
133
|
}
|
|
134
134
|
const r = await httpFetch(String(url));
|
|
135
|
-
//
|
|
135
|
+
// If it's JSON/plain text — return as is
|
|
136
136
|
if (/application\/json|text\/plain|text\/markdown/i.test(r.contentType)) {
|
|
137
137
|
const trimmed = r.body.slice(0, limit);
|
|
138
138
|
return `HTTP ${r.status} ${r.url}\nContent-Type: ${r.contentType}\n\n${trimmed}`;
|
|
139
139
|
}
|
|
140
|
-
// HTML —
|
|
140
|
+
// HTML — clean it up
|
|
141
141
|
const text = htmlToText(r.body);
|
|
142
|
-
//
|
|
142
|
+
// If there's almost no text — probably a JS page, give a hint
|
|
143
143
|
if (text.length < 200) {
|
|
144
144
|
return (`HTTP ${r.status} ${r.url}\n` +
|
|
145
145
|
`Content-Type: ${r.contentType}\n\n` +
|
|
@@ -177,13 +177,13 @@ export function createWebTools() {
|
|
|
177
177
|
if (r.status !== 200) {
|
|
178
178
|
return `DuckDuckGo вернул HTTP ${r.status}`;
|
|
179
179
|
}
|
|
180
|
-
//
|
|
180
|
+
// We parse with simple regexes. The html.duckduckgo.com format is stable.
|
|
181
181
|
const results = [];
|
|
182
182
|
const re = /<a[^>]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?(?:<a[^>]*class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>)?/gi;
|
|
183
183
|
let m;
|
|
184
184
|
while ((m = re.exec(r.body)) && results.length < limit) {
|
|
185
185
|
let href = m[1];
|
|
186
|
-
// DuckDuckGo
|
|
186
|
+
// DuckDuckGo wraps links in a redirect like /l/?uddg=...
|
|
187
187
|
const uddgMatch = href.match(/[?&]uddg=([^&]+)/);
|
|
188
188
|
if (uddgMatch)
|
|
189
189
|
href = decodeURIComponent(uddgMatch[1]);
|