zames_pro 1.0.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/LICENSE +21 -0
- package/README.md +48 -0
- package/package.json +36 -0
- package/src/agent-loop.js +419 -0
- package/src/browser.js +468 -0
- package/src/config.js +80 -0
- package/src/confirm.js +85 -0
- package/src/diff.js +124 -0
- package/src/gitTools.js +256 -0
- package/src/index.js +792 -0
- package/src/self-review.js +304 -0
- package/src/spinner.js +55 -0
- package/src/system-prompt.js +84 -0
- package/src/tools.js +206 -0
- package/src/transcript.js +46 -0
- package/src/undo.js +101 -0
- package/src/web.js +266 -0
package/src/diff.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
export function unifiedDiff(
|
|
2
|
+
oldText,
|
|
3
|
+
newText,
|
|
4
|
+
{ context = 3, label = '' } = {},
|
|
5
|
+
) {
|
|
6
|
+
const a = (oldText ?? '').split('\n')
|
|
7
|
+
const b = (newText ?? '').split('\n')
|
|
8
|
+
|
|
9
|
+
if (oldText === newText) return '(без изменений)'
|
|
10
|
+
|
|
11
|
+
const ops = diffOps(a, b)
|
|
12
|
+
const out = []
|
|
13
|
+
if (label) out.push(`--- ${label}`)
|
|
14
|
+
|
|
15
|
+
let i = 0
|
|
16
|
+
while (i < ops.length) {
|
|
17
|
+
const op = ops[i]
|
|
18
|
+
|
|
19
|
+
if (op.type === 'same') {
|
|
20
|
+
let j = i
|
|
21
|
+
while (j < ops.length && ops[j].type === 'same') j++
|
|
22
|
+
const blockLen = j - i
|
|
23
|
+
|
|
24
|
+
if (i === 0 && j === ops.length) {
|
|
25
|
+
i = j
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (i === 0) {
|
|
30
|
+
const end = Math.min(j, i + context)
|
|
31
|
+
for (let k = i; k < end; k++) out.push(' ' + ops[k].line)
|
|
32
|
+
if (end < j) out.push('...')
|
|
33
|
+
} else if (j === ops.length) {
|
|
34
|
+
const start = Math.max(i, j - context)
|
|
35
|
+
if (start > i) out.push('...')
|
|
36
|
+
for (let k = start; k < j; k++) out.push(' ' + ops[k].line)
|
|
37
|
+
} else {
|
|
38
|
+
const start = Math.max(i, j - context)
|
|
39
|
+
const end = Math.min(j, i + context)
|
|
40
|
+
if (start > i) out.push('...')
|
|
41
|
+
for (let k = start; k < end; k++) out.push(' ' + ops[k].line)
|
|
42
|
+
if (end < j) out.push('...')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
i = j
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const blockStart = i
|
|
50
|
+
let j = i
|
|
51
|
+
while (j < ops.length && ops[j].type !== 'same') j++
|
|
52
|
+
|
|
53
|
+
const preStart = Math.max(0, blockStart - context)
|
|
54
|
+
if (preStart < blockStart) {
|
|
55
|
+
if (preStart > 0) out.push('...')
|
|
56
|
+
for (let k = preStart; k < blockStart; k++) {
|
|
57
|
+
out.push(' ' + ops[k].line)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (let k = blockStart; k < j; k++) {
|
|
62
|
+
const o = ops[k]
|
|
63
|
+
out.push((o.type === 'del' ? '-' : '+') + o.line)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const postEnd = Math.min(ops.length, j + context)
|
|
67
|
+
for (let k = j; k < postEnd; k++) {
|
|
68
|
+
out.push(' ' + ops[k].line)
|
|
69
|
+
}
|
|
70
|
+
if (postEnd < ops.length) out.push('...')
|
|
71
|
+
|
|
72
|
+
i = j
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return out.join('\n')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function colorDiff(diffText, chalk) {
|
|
79
|
+
return diffText
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((line) => {
|
|
82
|
+
if (line.startsWith('+++') || line.startsWith('---'))
|
|
83
|
+
return chalk.bold(line)
|
|
84
|
+
if (line.startsWith('+')) return chalk.green(line)
|
|
85
|
+
if (line.startsWith('-')) return chalk.red(line)
|
|
86
|
+
if (line.startsWith('...')) return chalk.gray(line)
|
|
87
|
+
return line
|
|
88
|
+
})
|
|
89
|
+
.join('\n')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function diffOps(a, b) {
|
|
93
|
+
const n = a.length
|
|
94
|
+
const m = b.length
|
|
95
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1))
|
|
96
|
+
|
|
97
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
98
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
99
|
+
if (a[i] === b[j]) dp[i][j] = dp[i + 1][j + 1] + 1
|
|
100
|
+
else dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ops = []
|
|
105
|
+
let i = 0
|
|
106
|
+
let j = 0
|
|
107
|
+
while (i < n && j < m) {
|
|
108
|
+
if (a[i] === b[j]) {
|
|
109
|
+
ops.push({ type: 'same', line: a[i] })
|
|
110
|
+
i++
|
|
111
|
+
j++
|
|
112
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
113
|
+
ops.push({ type: 'del', line: a[i] })
|
|
114
|
+
i++
|
|
115
|
+
} else {
|
|
116
|
+
ops.push({ type: 'add', line: b[j] })
|
|
117
|
+
j++
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
while (i < n) ops.push({ type: 'del', line: a[i++] })
|
|
121
|
+
while (j < m) ops.push({ type: 'add', line: b[j++] })
|
|
122
|
+
|
|
123
|
+
return ops
|
|
124
|
+
}
|
package/src/gitTools.js
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { exec } from 'child_process'
|
|
2
|
+
|
|
3
|
+
function runGit(cmd, cwd, timeout = 15_000) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
exec(
|
|
6
|
+
cmd,
|
|
7
|
+
{
|
|
8
|
+
cwd,
|
|
9
|
+
timeout,
|
|
10
|
+
windowsHide: true,
|
|
11
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
12
|
+
env: {
|
|
13
|
+
...process.env,
|
|
14
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
15
|
+
GIT_PAGER: 'cat',
|
|
16
|
+
PAGER: 'cat',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
(err, stdout, stderr) => {
|
|
20
|
+
const out = (stdout || '').toString()
|
|
21
|
+
const errStr = (stderr || '').toString()
|
|
22
|
+
|
|
23
|
+
if (!err) {
|
|
24
|
+
const combined = (out + errStr).trim()
|
|
25
|
+
resolve(combined || '(команда выполнена, вывода нет)')
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const parts = []
|
|
30
|
+
if (err.killed) parts.push(`⏱ Таймаут ${timeout}ms`)
|
|
31
|
+
else if (err.code !== undefined && err.code !== null)
|
|
32
|
+
parts.push(`Exit code: ${err.code}`)
|
|
33
|
+
else parts.push(`Ошибка: ${err.message}`)
|
|
34
|
+
if (out.trim()) parts.push(out.trim())
|
|
35
|
+
if (errStr.trim()) parts.push(errStr.trim())
|
|
36
|
+
resolve(parts.join('\n'))
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Безопасно квотит пути/аргументы для передачи в shell.
|
|
43
|
+
// Строка или массив -> строка с двойными кавычками и экранированием.
|
|
44
|
+
function quoteArgs(input) {
|
|
45
|
+
const list = Array.isArray(input) ? input : [input]
|
|
46
|
+
return list
|
|
47
|
+
.map((v) => String(v).trim())
|
|
48
|
+
.filter(Boolean)
|
|
49
|
+
.map((v) => JSON.stringify(v))
|
|
50
|
+
.join(' ')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Проверяет, является ли директория git-репозиторием, и собирает контекст.
|
|
54
|
+
export async function getGitContext(workdir) {
|
|
55
|
+
const probe = await runGit(
|
|
56
|
+
'git rev-parse --is-inside-work-tree',
|
|
57
|
+
workdir,
|
|
58
|
+
5000,
|
|
59
|
+
)
|
|
60
|
+
if (probe.trim() !== 'true') return null
|
|
61
|
+
|
|
62
|
+
const branchR = await runGit('git branch --show-current', workdir, 5000)
|
|
63
|
+
const statusR = await runGit('git status --porcelain', workdir, 5000)
|
|
64
|
+
const remoteR = await runGit('git remote', workdir, 5000)
|
|
65
|
+
const aheadR = await runGit(
|
|
66
|
+
'git rev-list --left-right --count @{upstream}...HEAD',
|
|
67
|
+
workdir,
|
|
68
|
+
5000,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
const branch = branchR.trim() || '(detached HEAD)'
|
|
72
|
+
const statusLines = statusR
|
|
73
|
+
.split('\n')
|
|
74
|
+
.map((l) => l.trimEnd())
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
const remotes = remoteR
|
|
77
|
+
.split('\n')
|
|
78
|
+
.map((l) => l.trim())
|
|
79
|
+
.filter(Boolean)
|
|
80
|
+
const hasOrigin = remotes.includes('origin')
|
|
81
|
+
|
|
82
|
+
let ahead = 0
|
|
83
|
+
let behind = 0
|
|
84
|
+
const m = aheadR.match(/^(\d+)\s+(\d+)/)
|
|
85
|
+
if (m) {
|
|
86
|
+
behind = Number(m[1])
|
|
87
|
+
ahead = Number(m[2])
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
branch,
|
|
92
|
+
changedFiles: statusLines.length,
|
|
93
|
+
statusPreview: statusLines.slice(0, 30).join('\n'),
|
|
94
|
+
hasOrigin,
|
|
95
|
+
ahead,
|
|
96
|
+
behind,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function formatGitContext(ctx) {
|
|
101
|
+
if (!ctx) return 'Not a git repository (or git is not installed).'
|
|
102
|
+
|
|
103
|
+
let s = `- Branch: \`${ctx.branch}\``
|
|
104
|
+
if (ctx.hasOrigin) {
|
|
105
|
+
s += `\n- Remote: origin (ahead ${ctx.ahead}, behind ${ctx.behind})`
|
|
106
|
+
} else {
|
|
107
|
+
s += '\n- Remote: (none)'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (ctx.changedFiles > 0) {
|
|
111
|
+
s += `\n- Uncommitted changes: ${ctx.changedFiles} file(s)\n`
|
|
112
|
+
s += '```\n' + ctx.statusPreview + '\n```'
|
|
113
|
+
} else {
|
|
114
|
+
s += '\n- Working tree: clean'
|
|
115
|
+
}
|
|
116
|
+
return s
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createGitTools(workdir) {
|
|
120
|
+
const ensureRepo = async () => {
|
|
121
|
+
const probe = await runGit(
|
|
122
|
+
'git rev-parse --is-inside-work-tree',
|
|
123
|
+
workdir,
|
|
124
|
+
5000,
|
|
125
|
+
)
|
|
126
|
+
if (probe.trim() !== 'true') {
|
|
127
|
+
return (
|
|
128
|
+
`Not a git repository: ${workdir}\n` +
|
|
129
|
+
`Hint: run \`git init\` here, or cd into an existing repo.\n` +
|
|
130
|
+
`(raw probe output: ${JSON.stringify(probe.trim())})`
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return [
|
|
137
|
+
{
|
|
138
|
+
name: 'GitStatus',
|
|
139
|
+
description:
|
|
140
|
+
'Показать git status рабочей директории. Возвращает ошибку, если это не git-репозиторий.',
|
|
141
|
+
parameters: {},
|
|
142
|
+
fn: async () => {
|
|
143
|
+
const err = await ensureRepo()
|
|
144
|
+
if (err) return err
|
|
145
|
+
return runGit('git status', workdir, 10_000)
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
{
|
|
150
|
+
name: 'GitDiff',
|
|
151
|
+
description:
|
|
152
|
+
'Показать git diff. По умолчанию незастейдженные изменения. Можно ограничить path и указать staged=true.',
|
|
153
|
+
parameters: { path: 'string?', staged: 'boolean?' },
|
|
154
|
+
fn: async ({ path: p, staged }) => {
|
|
155
|
+
const err = await ensureRepo()
|
|
156
|
+
if (err) return err
|
|
157
|
+
const parts = ['git diff']
|
|
158
|
+
if (staged) parts.push('--staged')
|
|
159
|
+
if (p) parts.push('--', JSON.stringify(p))
|
|
160
|
+
return runGit(parts.join(' '), workdir, 20_000)
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
{
|
|
165
|
+
name: 'GitLog',
|
|
166
|
+
description: 'Показать последние N коммитов (по умолчанию 10).',
|
|
167
|
+
parameters: { count: 'number?' },
|
|
168
|
+
fn: async ({ count }) => {
|
|
169
|
+
const err = await ensureRepo()
|
|
170
|
+
if (err) return err
|
|
171
|
+
return runGit(
|
|
172
|
+
`git log --oneline --decorate -n ${Number(count) || 10}`,
|
|
173
|
+
workdir,
|
|
174
|
+
10_000,
|
|
175
|
+
)
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
name: 'GitAdd',
|
|
181
|
+
description:
|
|
182
|
+
'Добавить файлы в индекс. Если paths не задан — git add -A (всё).',
|
|
183
|
+
parameters: { paths: 'string?' },
|
|
184
|
+
fn: async ({ paths }) => {
|
|
185
|
+
const err = await ensureRepo()
|
|
186
|
+
if (err) return err
|
|
187
|
+
return runGit(
|
|
188
|
+
paths && String(paths).trim()
|
|
189
|
+
? 'git add -- ' + quoteArgs(paths)
|
|
190
|
+
: 'git add -A',
|
|
191
|
+
workdir,
|
|
192
|
+
20_000,
|
|
193
|
+
)
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
{
|
|
198
|
+
name: 'GitCommit',
|
|
199
|
+
description:
|
|
200
|
+
'Закоммитить застейдженное (если ничего не застейджено — сначала git add -A). Не пушит.',
|
|
201
|
+
parameters: { message: 'string' },
|
|
202
|
+
fn: async ({ message }) => {
|
|
203
|
+
const err = await ensureRepo()
|
|
204
|
+
if (err) return err
|
|
205
|
+
|
|
206
|
+
if (!message || !message.trim()) {
|
|
207
|
+
return 'Ошибка: message пустой.'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Индексируем всё только если в индексе пусто (как обещает описание).
|
|
211
|
+
const staged = await runGit('git diff --cached --name-only', workdir, 10_000)
|
|
212
|
+
const hasStaged = !/^\s*$/.test(staged) && staged.trim() !== '(команда выполнена, вывода нет)'
|
|
213
|
+
const addResult = hasStaged
|
|
214
|
+
? '(индекс уже не пуст — add -A пропущен)'
|
|
215
|
+
: await runGit('git add -A', workdir, 20_000)
|
|
216
|
+
|
|
217
|
+
const fs = await import('fs/promises')
|
|
218
|
+
const path = await import('path')
|
|
219
|
+
const os = await import('os')
|
|
220
|
+
const tmp = path.join(os.tmpdir(), `dsa-commit-${Date.now()}.txt`)
|
|
221
|
+
await fs.writeFile(tmp, message, 'utf-8')
|
|
222
|
+
try {
|
|
223
|
+
const result = await runGit(
|
|
224
|
+
`git commit -F ${JSON.stringify(tmp)}`,
|
|
225
|
+
workdir,
|
|
226
|
+
30_000,
|
|
227
|
+
)
|
|
228
|
+
return `add: ${addResult}\ncommit: ${result}`
|
|
229
|
+
} finally {
|
|
230
|
+
await fs.unlink(tmp).catch(() => {})
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
{
|
|
236
|
+
name: 'GitPush',
|
|
237
|
+
description:
|
|
238
|
+
'Запушить ветку в origin. Требует настроенных креденшелов (SSH или Windows Credential Manager).',
|
|
239
|
+
parameters: { branch: 'string?', setUpstream: 'boolean?' },
|
|
240
|
+
fn: async ({ branch, setUpstream }) => {
|
|
241
|
+
const err = await ensureRepo()
|
|
242
|
+
if (err) return err
|
|
243
|
+
|
|
244
|
+
const b =
|
|
245
|
+
branch && branch.trim()
|
|
246
|
+
? branch.trim()
|
|
247
|
+
: (await runGit('git branch --show-current', workdir, 5000)).trim()
|
|
248
|
+
if (!b) return 'Ошибка: не удалось определить ветку для push.'
|
|
249
|
+
|
|
250
|
+
const flag = setUpstream ? '-u ' : ''
|
|
251
|
+
const target = quoteArgs(b)
|
|
252
|
+
return runGit(`git push ${flag}origin ${target}`, workdir, 120_000)
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
]
|
|
256
|
+
}
|