dsh-desktop-kit 2.2.1 → 2.7.3
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/CHANGELOG.md +86 -0
- package/README.md +114 -27
- package/client.js +3270 -32
- package/docs/AGENT-HARNESS.md +144 -0
- package/docs/ARCHITECTURE.md +38 -7
- package/docs/COMPATIBILITY.md +4 -2
- package/docs/DEVELOPMENT.md +56 -0
- package/docs/INSTALL.md +20 -11
- package/docs/LAPTOP-INSTALL.md +96 -0
- package/docs/ROADMAP-3.0.md +353 -0
- package/docs/ROADMAP.md +1 -1
- package/docs/SUBAGENTS.md +25 -0
- package/docs/TEST-0.2.2.md +5 -5
- package/docs/TEST-2.3.0.md +49 -0
- package/docs/TEST-2.7.2.md +46 -0
- package/docs/TEST-2.7.3.md +20 -0
- package/docs/TOKENS.md +2 -2
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/UI-QA.md +28 -0
- package/docs/images/peak-toast-peak.png +0 -0
- package/docs/images/peak-toast-soon.png +0 -0
- package/docs/preview/peak-toast.html +155 -0
- package/index.js +18 -1
- package/package.json +26 -6
- package/plugins/dsh-agent-extensions/README.md +107 -0
- package/plugins/dsh-agent-extensions/client.js +157 -0
- package/plugins/dsh-agent-extensions/index.js +174 -0
- package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
- package/plugins/dsh-agent-extensions/lib/api.js +53 -0
- package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
- package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
- package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
- package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
- package/plugins/dsh-agent-extensions/lib/project.js +144 -0
- package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
- package/plugins/dsh-agent-extensions/package.json +20 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-1440.png +0 -0
- package/plugins/dsh-agent-extensions/screenshots/agents-390.png +0 -0
- package/plugins/dsh-browser-qa/README.md +221 -0
- package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
- package/plugins/dsh-browser-qa/index.js +165 -0
- package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
- package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
- package/plugins/dsh-browser-qa/lib/pages.js +552 -0
- package/plugins/dsh-browser-qa/lib/qa.js +827 -0
- package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
- package/plugins/dsh-browser-qa/package.json +31 -0
- package/plugins/dsh-checkpoints/README.md +22 -0
- package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
- package/plugins/dsh-checkpoints/index.js +80 -0
- package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
- package/plugins/dsh-checkpoints/package.json +25 -0
- package/plugins/dsh-github-review/README.md +131 -0
- package/plugins/dsh-github-review/cordis.patch.yml +3 -0
- package/plugins/dsh-github-review/index.js +536 -0
- package/plugins/dsh-github-review/lib/gh.js +494 -0
- package/plugins/dsh-github-review/lib/normalize.js +662 -0
- package/plugins/dsh-github-review/package.json +28 -0
- package/plugins/dsh-guardrails/README.md +264 -0
- package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
- package/plugins/dsh-guardrails/index.js +389 -0
- package/plugins/dsh-guardrails/lib/hooks.js +262 -0
- package/plugins/dsh-guardrails/lib/modes.js +77 -0
- package/plugins/dsh-guardrails/lib/policy.js +592 -0
- package/plugins/dsh-guardrails/lib/scan.js +451 -0
- package/plugins/dsh-guardrails/lib/secrets.js +274 -0
- package/plugins/dsh-guardrails/package.json +26 -0
- package/plugins/dsh-mcp-manager/README.md +78 -0
- package/plugins/dsh-mcp-manager/client.js +373 -0
- package/plugins/dsh-mcp-manager/index.js +159 -0
- package/plugins/dsh-mcp-manager/lib/api.js +250 -0
- package/plugins/dsh-mcp-manager/lib/client.js +219 -0
- package/plugins/dsh-mcp-manager/lib/config.js +217 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
- package/plugins/dsh-mcp-manager/package.json +18 -0
- package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
- package/plugins/dsh-memory-bridge/README.md +161 -0
- package/plugins/dsh-memory-bridge/client.js +2264 -0
- package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
- package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
- package/plugins/dsh-memory-bridge/icon.svg +13 -0
- package/plugins/dsh-memory-bridge/index.js +378 -0
- package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
- package/plugins/dsh-memory-bridge/lib/github.js +473 -0
- package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
- package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
- package/plugins/dsh-memory-bridge/lib/state.js +565 -0
- package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
- package/plugins/dsh-memory-bridge/locale/en.json +6 -0
- package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
- package/plugins/dsh-memory-bridge/package.json +67 -0
- package/plugins/dsh-peak-guard/README.md +16 -3
- package/plugins/dsh-peak-guard/client.js +223 -8
- package/plugins/dsh-peak-guard/package.json +1 -1
- package/plugins/dsh-plan-mode/README.md +24 -0
- package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
- package/plugins/dsh-plan-mode/index.js +103 -0
- package/plugins/dsh-plan-mode/lib/plan.js +89 -0
- package/plugins/dsh-plan-mode/package.json +18 -0
- package/plugins/dsh-profile-transfer/client.js +277 -17
- package/plugins/dsh-profile-transfer/index.js +10 -0
- package/plugins/dsh-profile-transfer/package.json +3 -1
- package/plugins/dsh-profile-transfer/transfer.js +101 -2
- package/plugins/dsh-project-memory/index.js +92 -2
- package/plugins/dsh-project-memory/package.json +1 -1
- package/plugins/dsh-repo-map/README.md +26 -0
- package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
- package/plugins/dsh-repo-map/index.js +99 -0
- package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
- package/plugins/dsh-repo-map/package.json +25 -0
- package/plugins/dsh-skill-autopilot/index.js +135 -11
- package/skills/delivery-gate/SKILL.md +31 -0
- package/skills/git-and-ci/SKILL.md +5 -1
- package/skills/manifest.json +31 -6
- package/skills/requirements-and-planning/SKILL.md +3 -0
- package/skills/research-first/SKILL.md +35 -0
- package/skills/research-first/references/query-patterns.md +20 -0
- package/skills/subagent-playbook/SKILL.md +36 -0
- package/skills/subagent-playbook/references/prompts.md +39 -0
- package/skills/ui-craft/SKILL.md +43 -0
- package/skills/visual-qa/SKILL.md +29 -0
- package/skills/visual-qa/references/commands.md +32 -0
- package/skills-baseline.json +219 -0
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-github-review — Host half.
|
|
3
|
+
*
|
|
4
|
+
* One model tool, `github_review`, over the official `gh` CLI:
|
|
5
|
+
* - read-only by default: status, pr, checks, comments, diff;
|
|
6
|
+
* - one opt-in mutation: comment, only with `confirm: true`, an explicit PR
|
|
7
|
+
* number and a non-empty body;
|
|
8
|
+
* - merge / close / reopen / force-push are deliberately not implemented and
|
|
9
|
+
* explicitly refused.
|
|
10
|
+
*
|
|
11
|
+
* All `gh` calls are argv arrays (no shell), time-bounded and scrubbed of
|
|
12
|
+
* tokens before anything reaches the model. The plugin never reads token
|
|
13
|
+
* files itself: authentication belongs to `gh auth login`.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from "node:fs";
|
|
17
|
+
import path from "node:path";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
LIMITS,
|
|
21
|
+
clampDiffBytes,
|
|
22
|
+
formatBytes,
|
|
23
|
+
normalizeChecks,
|
|
24
|
+
normalizeConversationComments,
|
|
25
|
+
normalizePullRequest,
|
|
26
|
+
normalizeReviewComments,
|
|
27
|
+
parsePrNumber,
|
|
28
|
+
summarizeReviews,
|
|
29
|
+
truncateDiff,
|
|
30
|
+
} from "./lib/normalize.js";
|
|
31
|
+
import {
|
|
32
|
+
detectRepo,
|
|
33
|
+
fetchChecks,
|
|
34
|
+
fetchComments,
|
|
35
|
+
fetchDiff,
|
|
36
|
+
fetchPrView,
|
|
37
|
+
postComment,
|
|
38
|
+
probeStatus,
|
|
39
|
+
resolvePrNumber,
|
|
40
|
+
} from "./lib/gh.js";
|
|
41
|
+
|
|
42
|
+
export const inject = ["tools"];
|
|
43
|
+
|
|
44
|
+
const TOOL_ACTIONS = new Set(["status", "pr", "checks", "comments", "diff", "comment"]);
|
|
45
|
+
const UNSUPPORTED_ACTIONS = new Set([
|
|
46
|
+
"merge",
|
|
47
|
+
"close",
|
|
48
|
+
"reopen",
|
|
49
|
+
"approve",
|
|
50
|
+
"request-changes",
|
|
51
|
+
"request_changes",
|
|
52
|
+
"push",
|
|
53
|
+
"force-push",
|
|
54
|
+
"force_push",
|
|
55
|
+
"rebase",
|
|
56
|
+
"edit",
|
|
57
|
+
"delete",
|
|
58
|
+
"ready",
|
|
59
|
+
]);
|
|
60
|
+
const RENDER_CHARS = 24_000;
|
|
61
|
+
const DIFF_RENDER_CHARS = 48_000;
|
|
62
|
+
|
|
63
|
+
const FAILURE_HINTS = {
|
|
64
|
+
"gh-missing": "gh CLI не найден. Установите GitHub CLI (https://cli.github.com) и выполните `gh auth login`.",
|
|
65
|
+
"not-authenticated": "gh не авторизован — выполните `gh auth login` (или задайте GH_TOKEN с доступом к репозиторию).",
|
|
66
|
+
"no-pr": "PR для текущей ветки не найден — укажите точный номер: pr=<номер>.",
|
|
67
|
+
"pr-not-found": "PR с таким номером не найден или нет доступа (проверьте номер и права токена).",
|
|
68
|
+
"repo-not-found": "Репозиторий не найден или нет доступа. Проверьте repo=owner/name и права токена.",
|
|
69
|
+
"rate-limited": "Сработал лимит GitHub API — повторите позже или проверьте токен.",
|
|
70
|
+
timeout: "gh не ответил за отведённое время (сеть или аутентификация).",
|
|
71
|
+
"bad-pr": "Некорректный номер PR: нужно целое положительное число.",
|
|
72
|
+
"bad-repo": "Некорректный repo: нужен owner/name или host/owner/name.",
|
|
73
|
+
"bad-cwd": "Рабочий каталог не найден.",
|
|
74
|
+
"bad-body": "Комментарий не отправлен: body нарушает лимит длины.",
|
|
75
|
+
"bad-json": "gh вернул неожиданный ответ — проверьте версию gh и авторизацию.",
|
|
76
|
+
"gh-failed": "gh завершился с ошибкой.",
|
|
77
|
+
internal: "Внутренняя ошибка плагина.",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/* ------------------------------------------------------------------ */
|
|
81
|
+
/* Session/argument helpers */
|
|
82
|
+
/* ------------------------------------------------------------------ */
|
|
83
|
+
|
|
84
|
+
function rootOf(args, exec) {
|
|
85
|
+
const explicit = typeof args?.cwd === "string" ? args.cwd.trim() : "";
|
|
86
|
+
const cwd = exec?.agent?.session?.header?.cwd;
|
|
87
|
+
return path.resolve(explicit || cwd || process.cwd());
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function truncate(text, max) {
|
|
91
|
+
const value = typeof text === "string" ? text : String(text ?? "");
|
|
92
|
+
return value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function repoLabel(spec) {
|
|
96
|
+
if (!spec) return "";
|
|
97
|
+
return spec.host === "github.com" ? spec.slug : `${spec.host}/${spec.slug}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function failureText(failure) {
|
|
101
|
+
const hint = FAILURE_HINTS[failure?.code] ?? FAILURE_HINTS["gh-failed"];
|
|
102
|
+
const detail = typeof failure?.detail === "string" && failure.detail.trim().length > 0
|
|
103
|
+
? `\n${truncate(failure.detail.trim(), 800)}`
|
|
104
|
+
: "";
|
|
105
|
+
return `${hint}${detail}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function failureResult(action, failure) {
|
|
109
|
+
return {
|
|
110
|
+
ok: false,
|
|
111
|
+
action,
|
|
112
|
+
code: failure?.code ?? "gh-failed",
|
|
113
|
+
error: failureText(failure),
|
|
114
|
+
text: `github_review ${action}: ${failureText(failure)}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Resolve cwd + optional repo override + optional explicit PR number. */
|
|
119
|
+
function prepareTarget({ args, cwd }) {
|
|
120
|
+
const detected = detectRepo({ cwd, repo: args?.repo });
|
|
121
|
+
if (detected.error) {
|
|
122
|
+
const code = /некорректный repo/iu.test(detected.error) ? "bad-repo" : "bad-cwd";
|
|
123
|
+
return { ok: false, failure: { code, detail: detected.error } };
|
|
124
|
+
}
|
|
125
|
+
const raw = args?.pr;
|
|
126
|
+
const provided = raw !== undefined && raw !== null && raw !== "";
|
|
127
|
+
const number = parsePrNumber(raw);
|
|
128
|
+
if (provided && number === undefined) {
|
|
129
|
+
return { ok: false, failure: { code: "bad-pr", detail: `pr=«${String(raw)}» не является номером PR` } };
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: true,
|
|
133
|
+
cwd,
|
|
134
|
+
repo: detected.repo ?? undefined,
|
|
135
|
+
detected,
|
|
136
|
+
number,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Explicit number wins; otherwise ask gh for the current branch PR. */
|
|
141
|
+
function ensureNumber(target) {
|
|
142
|
+
if (target.number !== undefined) return { ok: true, number: target.number };
|
|
143
|
+
return resolvePrNumber({ cwd: target.cwd, repo: target.repo, branch: target.detected?.branch });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* `no-pr` for an explicit number means "that PR is missing", not "no PR for
|
|
148
|
+
* the current branch" — keep the hint truthful.
|
|
149
|
+
*/
|
|
150
|
+
function targetFailure(failure, target) {
|
|
151
|
+
if (failure?.code === "no-pr" && target?.number !== undefined) return { ...failure, code: "pr-not-found" };
|
|
152
|
+
return failure;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ------------------------------------------------------------------ */
|
|
156
|
+
/* status */
|
|
157
|
+
/* ------------------------------------------------------------------ */
|
|
158
|
+
|
|
159
|
+
function statusAction(cwd, args) {
|
|
160
|
+
const status = probeStatus({ cwd, repo: args?.repo });
|
|
161
|
+
const { gh, auth } = status;
|
|
162
|
+
const detected = status.repo;
|
|
163
|
+
const lines = [];
|
|
164
|
+
lines.push(gh.installed ? `gh: установлен${gh.version ? ` (v${gh.version})` : ""}` : "gh: не установлен");
|
|
165
|
+
if (gh.installed) {
|
|
166
|
+
if (auth.authenticated) {
|
|
167
|
+
const active = auth.active;
|
|
168
|
+
const scopes = Array.isArray(active?.scopes) && active.scopes.length > 0 ? ` · scopes: ${active.scopes.join(", ")}` : "";
|
|
169
|
+
lines.push(`Авторизация: да — ${active?.login || "аккаунт"}${active?.host ? ` @ ${active.host}` : ""}${scopes}`);
|
|
170
|
+
if (auth.accounts.length > 1) lines.push(`Аккаунтов в gh: ${auth.accounts.length} (активен ${active?.login || "?"})`);
|
|
171
|
+
} else {
|
|
172
|
+
lines.push("Авторизация: нет — выполните `gh auth login`");
|
|
173
|
+
}
|
|
174
|
+
if (auth.detail && !auth.authenticated) lines.push(`Детали: ${truncate(auth.detail, 400)}`);
|
|
175
|
+
} else {
|
|
176
|
+
lines.push("Установите GitHub CLI: https://cli.github.com — затем `gh auth login`.");
|
|
177
|
+
if (gh.detail) lines.push(`Детали: ${truncate(gh.detail, 400)}`);
|
|
178
|
+
}
|
|
179
|
+
if (detected.error) {
|
|
180
|
+
lines.push(`Репозиторий: ${detected.error}`);
|
|
181
|
+
} else if (detected.repo) {
|
|
182
|
+
lines.push(`Репозиторий: ${repoLabel(detected.repo)}${detected.branch ? ` · ветка ${detected.branch}` : ""}${detected.source === "explicit" ? " · из repo=" : " · из git remote"}`);
|
|
183
|
+
} else {
|
|
184
|
+
lines.push("Репозиторий: не определён — cwd не git-репозиторий или у origin нет GitHub-remote. Укажите repo=owner/name.");
|
|
185
|
+
}
|
|
186
|
+
if (detected.root) lines.push(`Корень: ${detected.root}`);
|
|
187
|
+
if (!detected.gitInstalled) lines.push("git: не найден в PATH — определение репозитория по remote недоступно.");
|
|
188
|
+
if (gh.installed && auth.authenticated) lines.push("Дальше: action=pr | checks | comments | diff (pr=<номер> опционально), action=comment (только с confirm: true).");
|
|
189
|
+
return {
|
|
190
|
+
ok: gh.installed && auth.authenticated && !detected.error,
|
|
191
|
+
action: "status",
|
|
192
|
+
cwd,
|
|
193
|
+
gh: { installed: gh.installed, version: gh.version },
|
|
194
|
+
auth: {
|
|
195
|
+
authenticated: auth.authenticated,
|
|
196
|
+
notLoggedIn: auth.notLoggedIn,
|
|
197
|
+
host: auth.host,
|
|
198
|
+
accounts: auth.accounts,
|
|
199
|
+
active: auth.active,
|
|
200
|
+
scopes: auth.scopes,
|
|
201
|
+
},
|
|
202
|
+
repository: detected.repo
|
|
203
|
+
? {
|
|
204
|
+
slug: detected.repo.slug,
|
|
205
|
+
host: detected.repo.host,
|
|
206
|
+
url: detected.repo.webUrl,
|
|
207
|
+
source: detected.source,
|
|
208
|
+
branch: detected.branch ?? null,
|
|
209
|
+
root: detected.root ?? null,
|
|
210
|
+
}
|
|
211
|
+
: null,
|
|
212
|
+
text: lines.join("\n"),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/* ------------------------------------------------------------------ */
|
|
217
|
+
/* pr / checks / comments / diff renderers */
|
|
218
|
+
/* ------------------------------------------------------------------ */
|
|
219
|
+
|
|
220
|
+
function renderPr(pr) {
|
|
221
|
+
const lines = [];
|
|
222
|
+
const flags = [pr.state.toLowerCase() || "state?", pr.isDraft ? "draft" : ""].filter(Boolean);
|
|
223
|
+
lines.push(`PR #${pr.number ?? "?"} · ${flags.join(" · ")} · «${pr.title || "(без заголовка)"}»`);
|
|
224
|
+
if (pr.url) lines.push(`URL: ${pr.url}`);
|
|
225
|
+
lines.push(`Автор: ${pr.author || "неизвестен"}${pr.createdAt ? ` · создан ${pr.createdAt}` : ""}${pr.updatedAt ? ` · обновлён ${pr.updatedAt}` : ""}`);
|
|
226
|
+
lines.push(`Ветки: ${pr.base.branch || "?"} ← ${pr.head.branch || "?"}`);
|
|
227
|
+
lines.push(`Изменения: ${pr.changedFiles} файлов, +${pr.additions} −${pr.deletions}, коммитов ${pr.commits.total}`);
|
|
228
|
+
const meta = [];
|
|
229
|
+
if (pr.reviewDecision) meta.push(`review: ${pr.reviewDecision}`);
|
|
230
|
+
if (pr.mergeable) meta.push(`mergeable: ${pr.mergeable}`);
|
|
231
|
+
if (pr.labels.length > 0) meta.push(`labels: ${pr.labels.map((label) => label.name).join(", ")}`);
|
|
232
|
+
if (meta.length > 0) lines.push(meta.join(" · "));
|
|
233
|
+
if (pr.files.total > 0) {
|
|
234
|
+
lines.push(`Файлы (${pr.files.total}${pr.files.truncated ? `, показаны ${pr.files.items.length}` : ""}):`);
|
|
235
|
+
for (const file of pr.files.items) {
|
|
236
|
+
const type = file.changeType ? ` [${file.changeType}]` : "";
|
|
237
|
+
const diff = file.additions || file.deletions ? ` (+${file.additions} −${file.deletions})` : "";
|
|
238
|
+
lines.push(`- ${file.path || "(без пути)"}${type}${diff}`);
|
|
239
|
+
}
|
|
240
|
+
if (pr.files.truncated) lines.push(`- … ещё ${pr.files.total - pr.files.items.length} файлов`);
|
|
241
|
+
}
|
|
242
|
+
if (pr.commits.total > 0) {
|
|
243
|
+
lines.push(`Коммиты (${pr.commits.total}${pr.commits.truncated ? `, показаны ${pr.commits.items.length}` : ""}):`);
|
|
244
|
+
for (const commit of pr.commits.items) {
|
|
245
|
+
lines.push(`- ${commit.shortOid || "?"} ${commit.messageHeadline || "(без сообщения)"}${commit.author ? ` — ${commit.author}` : ""}${commit.committedDate ? ` · ${commit.committedDate}` : ""}`);
|
|
246
|
+
}
|
|
247
|
+
if (pr.commits.truncated) lines.push(`- … ещё ${pr.commits.total - pr.commits.items.length} коммитов`);
|
|
248
|
+
}
|
|
249
|
+
if (!pr.body.empty) {
|
|
250
|
+
lines.push("Body:");
|
|
251
|
+
lines.push(pr.body.text.trim());
|
|
252
|
+
if (pr.body.truncated) lines.push(`… body обрезан (${pr.body.chars} символов)`);
|
|
253
|
+
}
|
|
254
|
+
return lines.join("\n");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function renderChecks(number, checks, format) {
|
|
258
|
+
const { summary } = checks;
|
|
259
|
+
const lines = [];
|
|
260
|
+
lines.push(`Checks PR #${number}: ${checks.total} проверок${checks.truncated ? ` (показаны ${checks.items.length})` : ""}${format === "text" ? " · формат: текст (gh без --json)" : ""}`);
|
|
261
|
+
lines.push(`pass ${summary.pass} · fail ${summary.fail} · pending ${summary.pending} · skipping ${summary.skipping} · cancel ${summary.cancel}${summary.unknown ? ` · unknown ${summary.unknown}` : ""}`);
|
|
262
|
+
if (checks.failed.length > 0) {
|
|
263
|
+
lines.push("Провалено:");
|
|
264
|
+
for (const item of checks.failed) {
|
|
265
|
+
const workflow = item.workflow ? ` (${item.workflow})` : "";
|
|
266
|
+
const description = item.description ? `: ${item.description}` : "";
|
|
267
|
+
const link = item.link ? ` → ${item.link}` : "";
|
|
268
|
+
lines.push(`- ${item.name}${workflow} — ${item.state || "FAIL"}${description}${link}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (checks.pending.length > 0) {
|
|
272
|
+
lines.push("В процессе:");
|
|
273
|
+
for (const item of checks.pending) {
|
|
274
|
+
const link = item.link ? ` → ${item.link}` : "";
|
|
275
|
+
lines.push(`- ${item.name} — ${item.state || "PENDING"}${link}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const rest = checks.items.filter((item) => item.bucket !== "fail" && item.bucket !== "pending");
|
|
279
|
+
if (rest.length > 0) {
|
|
280
|
+
const shown = rest.slice(0, 15);
|
|
281
|
+
lines.push(`Остальные (${rest.length}): ${shown.map((item) => `${item.name}${item.state ? ` [${item.state}]` : ""}`).join(", ")}${rest.length > shown.length ? ", …" : ""}`);
|
|
282
|
+
}
|
|
283
|
+
if (checks.total === 0) lines.push("Проверки не найдены (возможно, CI не настроен или PR без workflow).");
|
|
284
|
+
return lines.join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderComments(number, viewJson, reviews, conversation, threads, inline) {
|
|
288
|
+
const title = viewJson && typeof viewJson.title === "string" ? viewJson.title : "";
|
|
289
|
+
const lines = [];
|
|
290
|
+
lines.push(`Комментарии PR #${number}${title ? ` · «${title}»` : ""}`);
|
|
291
|
+
lines.push(
|
|
292
|
+
`Review: ${reviews.total} · approved ${reviews.byState.APPROVED} · changes requested ${reviews.byState.CHANGES_REQUESTED} · commented ${reviews.byState.COMMENTED} · dismissed ${reviews.byState.DISMISSED}${reviews.byState.OTHER ? ` · other ${reviews.byState.OTHER}` : ""}`,
|
|
293
|
+
);
|
|
294
|
+
if (reviews.blocking.length > 0) {
|
|
295
|
+
lines.push("Запрошены изменения:");
|
|
296
|
+
for (const review of reviews.blocking) {
|
|
297
|
+
lines.push(`- @${review.author || "?"}${review.submittedAt ? ` (${review.submittedAt})` : ""}: ${review.body || "(без текста)"}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (conversation.total > 0) {
|
|
301
|
+
lines.push(`Обсуждение: ${conversation.total} комментариев${conversation.truncated ? ` (показаны ${conversation.items.length})` : ""}`);
|
|
302
|
+
for (const item of conversation.items.slice(0, 10)) {
|
|
303
|
+
lines.push(`- @${item.author || "?"}${item.createdAt ? ` (${item.createdAt})` : ""}: ${item.body || "(пусто)"}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const replyNote = threads.replies > 0 ? ` · ответов ${threads.replies}` : "";
|
|
307
|
+
const truncatedNote = threads.truncated ? ` (показаны ${threads.threads.length})` : "";
|
|
308
|
+
lines.push(`Inline threads: ${threads.threadCount}${threads.total > 0 ? ` · комментариев ${threads.total}` : ""}${replyNote}${truncatedNote}`);
|
|
309
|
+
if (!inline.ok) lines.push(`Inline-комментарии недоступны: ${inline.failure?.detail || inline.failure?.code || "ошибка API"}`);
|
|
310
|
+
if (threads.byFile.length > 0) {
|
|
311
|
+
lines.push(`Файлы с замечаниями: ${threads.byFile.map((entry) => `${entry.path} (${entry.count})`).join(", ")}`);
|
|
312
|
+
}
|
|
313
|
+
for (const thread of threads.threads) {
|
|
314
|
+
const location = thread.path ? `${thread.path}${thread.line ? `:${thread.line}` : ""}` : "(без файла)";
|
|
315
|
+
const replies = thread.replies.length > 0 ? ` · ответов: ${thread.replies.length}` : "";
|
|
316
|
+
lines.push(`- ${location} · @${thread.author || "?"}: ${thread.body || "(пусто)"}${replies}`);
|
|
317
|
+
}
|
|
318
|
+
if (threads.resolvedKnown === false && threads.threadCount > 0) {
|
|
319
|
+
lines.push("Примечание: resolved/outdated у inline-тредов REST API не отдаёт — состояние не показано.");
|
|
320
|
+
}
|
|
321
|
+
if (threads.total >= LIMITS.inlineComments) {
|
|
322
|
+
lines.push(`Примечание: inline-комментариев ≥ ${LIMITS.inlineComments} — показаны не все (REST-лимит плагина).`);
|
|
323
|
+
}
|
|
324
|
+
return lines.join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function renderDiff(number, diff) {
|
|
328
|
+
const lines = [];
|
|
329
|
+
const size = diff.truncated
|
|
330
|
+
? `diff больше лимита ${formatBytes(diff.limit)} · показано ${formatBytes(diff.shownBytes)}`
|
|
331
|
+
: `Размер: ${formatBytes(diff.bytes)}`;
|
|
332
|
+
lines.push(`Diff PR #${number} — ${size}`);
|
|
333
|
+
lines.push("");
|
|
334
|
+
if (diff.text.length === 0) lines.push("(diff пуст — изменений нет)");
|
|
335
|
+
else lines.push(diff.text);
|
|
336
|
+
if (diff.truncated) {
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push("… diff обрезан по байтовому лимиту. Увеличьте maxBytes или смотрите отдельные файлы PR.");
|
|
339
|
+
}
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* ------------------------------------------------------------------ */
|
|
344
|
+
/* Actions */
|
|
345
|
+
/* ------------------------------------------------------------------ */
|
|
346
|
+
|
|
347
|
+
function prAction(target) {
|
|
348
|
+
const resolved = ensureNumber(target);
|
|
349
|
+
if (!resolved.ok) return failureResult("pr", resolved.failure);
|
|
350
|
+
const view = fetchPrView({ cwd: target.cwd, repo: target.repo, number: resolved.number });
|
|
351
|
+
if (!view.ok) return failureResult("pr", targetFailure(view.failure, target));
|
|
352
|
+
const pr = normalizePullRequest(view.json);
|
|
353
|
+
return {
|
|
354
|
+
ok: true,
|
|
355
|
+
action: "pr",
|
|
356
|
+
number: pr.number ?? resolved.number,
|
|
357
|
+
pr,
|
|
358
|
+
text: renderPr(pr),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function checksAction(target) {
|
|
363
|
+
const resolved = ensureNumber(target);
|
|
364
|
+
if (!resolved.ok) return failureResult("checks", resolved.failure);
|
|
365
|
+
const fetched = fetchChecks({ cwd: target.cwd, repo: target.repo, number: resolved.number });
|
|
366
|
+
if (!fetched.ok) return failureResult("checks", targetFailure(fetched.failure, target));
|
|
367
|
+
const checks = normalizeChecks(fetched.rows);
|
|
368
|
+
return {
|
|
369
|
+
ok: true,
|
|
370
|
+
action: "checks",
|
|
371
|
+
number: resolved.number,
|
|
372
|
+
format: fetched.format,
|
|
373
|
+
checks,
|
|
374
|
+
failed: checks.failed.length,
|
|
375
|
+
text: renderChecks(resolved.number, checks, fetched.format),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function commentsAction(target) {
|
|
380
|
+
const resolved = ensureNumber(target);
|
|
381
|
+
if (!resolved.ok) return failureResult("comments", resolved.failure);
|
|
382
|
+
const fetched = fetchComments({ cwd: target.cwd, repo: target.repo, number: resolved.number });
|
|
383
|
+
if (!fetched.ok) return failureResult("comments", targetFailure(fetched.failure, target));
|
|
384
|
+
const reviews = summarizeReviews(fetched.json?.reviews);
|
|
385
|
+
const conversation = normalizeConversationComments(fetched.json?.comments);
|
|
386
|
+
const threads = normalizeReviewComments(fetched.inline.rows);
|
|
387
|
+
return {
|
|
388
|
+
ok: true,
|
|
389
|
+
action: "comments",
|
|
390
|
+
number: resolved.number,
|
|
391
|
+
reviews,
|
|
392
|
+
conversation,
|
|
393
|
+
threads,
|
|
394
|
+
inline: { ok: fetched.inline.ok, failure: fetched.inline.failure },
|
|
395
|
+
text: renderComments(resolved.number, fetched.json, reviews, conversation, threads, fetched.inline),
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function diffAction(target, args) {
|
|
400
|
+
const resolved = ensureNumber(target);
|
|
401
|
+
if (!resolved.ok) return failureResult("diff", resolved.failure);
|
|
402
|
+
const limit = clampDiffBytes(args?.maxBytes);
|
|
403
|
+
const fetched = await fetchDiff({ cwd: target.cwd, repo: target.repo, number: resolved.number, maxBytes: limit });
|
|
404
|
+
if (!fetched.ok) return failureResult("diff", targetFailure(fetched.failure, target));
|
|
405
|
+
const capped = truncateDiff(fetched.text, limit);
|
|
406
|
+
const diff = {
|
|
407
|
+
text: capped.text,
|
|
408
|
+
bytes: capped.bytes,
|
|
409
|
+
shownBytes: capped.shownBytes,
|
|
410
|
+
truncated: capped.truncated || fetched.truncated === true,
|
|
411
|
+
limit,
|
|
412
|
+
};
|
|
413
|
+
return {
|
|
414
|
+
ok: true,
|
|
415
|
+
action: "diff",
|
|
416
|
+
number: resolved.number,
|
|
417
|
+
bytes: diff.bytes,
|
|
418
|
+
shownBytes: diff.shownBytes,
|
|
419
|
+
truncated: diff.truncated,
|
|
420
|
+
limit,
|
|
421
|
+
patch: diff.text,
|
|
422
|
+
text: renderDiff(resolved.number, diff),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function commentRefusal(action, reason) {
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
action,
|
|
430
|
+
code: "confirmation-required",
|
|
431
|
+
text: `github_review comment: отказ — ${reason}. Мутация не выполнена. Нужны одновременно confirm: true, точный целочисленный pr=<номер> и непустой body.`,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function commentAction(cwd, args) {
|
|
436
|
+
if (args?.confirm !== true) return commentRefusal("comment", "нет явного подтверждения confirm: true");
|
|
437
|
+
const number = parsePrNumber(args?.pr);
|
|
438
|
+
if (number === undefined) return commentRefusal("comment", "не указан точный номер PR (pr)");
|
|
439
|
+
if (typeof args?.body !== "string" || args.body.trim().length === 0) return commentRefusal("comment", "пустой body комментария");
|
|
440
|
+
const body = args.body.trim();
|
|
441
|
+
if (body.length > LIMITS.maxCommentChars) {
|
|
442
|
+
return failureResult("comment", { code: "bad-body", detail: `body ${body.length} символов > ${LIMITS.maxCommentChars}` });
|
|
443
|
+
}
|
|
444
|
+
const target = prepareTarget({ args, cwd });
|
|
445
|
+
if (!target.ok) return failureResult("comment", target.failure);
|
|
446
|
+
const posted = postComment({ cwd: target.cwd, repo: target.repo, number, body });
|
|
447
|
+
if (!posted.ok) return failureResult("comment", targetFailure(posted.failure, { number }));
|
|
448
|
+
return {
|
|
449
|
+
ok: true,
|
|
450
|
+
action: "comment",
|
|
451
|
+
number,
|
|
452
|
+
url: posted.url || null,
|
|
453
|
+
repo: target.repo ? repoLabel(target.repo) : null,
|
|
454
|
+
text: `Комментарий отправлен в PR #${number}${target.repo ? ` (${repoLabel(target.repo)})` : ""}${posted.url ? `: ${posted.url}` : ""}.`,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/* ------------------------------------------------------------------ */
|
|
459
|
+
/* Tool registration */
|
|
460
|
+
/* ------------------------------------------------------------------ */
|
|
461
|
+
|
|
462
|
+
const TOOL_PARAMETERS = {
|
|
463
|
+
type: "object",
|
|
464
|
+
additionalProperties: false,
|
|
465
|
+
properties: {
|
|
466
|
+
action: {
|
|
467
|
+
type: "string",
|
|
468
|
+
enum: ["status", "pr", "checks", "comments", "diff", "comment"],
|
|
469
|
+
description: "status | pr | checks | comments | diff | comment",
|
|
470
|
+
},
|
|
471
|
+
pr: { type: "integer", description: "PR number. Read actions default to the PR of the current branch; comment requires an explicit number." },
|
|
472
|
+
repo: { type: "string", description: "Repository override: owner/name or host/owner/name; defaults to the git origin remote of cwd." },
|
|
473
|
+
cwd: { type: "string", description: "Repository working directory; defaults to the current Session cwd." },
|
|
474
|
+
body: { type: "string", description: "Comment body for action=comment (Markdown, max 60000 characters)." },
|
|
475
|
+
confirm: { type: "boolean", description: "Explicit confirmation required by action=comment; without true the mutation is refused." },
|
|
476
|
+
maxBytes: { type: "integer", description: "Diff byte cap for action=diff (4096..2097152, default 204800)." },
|
|
477
|
+
},
|
|
478
|
+
required: ["action"],
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
async function runTool(args, exec) {
|
|
482
|
+
const action = typeof args?.action === "string" ? args.action.trim().toLowerCase() : "status";
|
|
483
|
+
const cwd = rootOf(args, exec);
|
|
484
|
+
try {
|
|
485
|
+
if (!fs.existsSync(cwd)) return failureResult(action, { code: "bad-cwd", detail: cwd });
|
|
486
|
+
if (action === "status") return statusAction(cwd, args);
|
|
487
|
+
if (action === "comment") return commentAction(cwd, args);
|
|
488
|
+
if (UNSUPPORTED_ACTIONS.has(action)) {
|
|
489
|
+
return {
|
|
490
|
+
ok: false,
|
|
491
|
+
action,
|
|
492
|
+
code: "unsupported-action",
|
|
493
|
+
text: `github_review: действие «${action}» не поддерживается. Плагин read-only по умолчанию и не делает merge/close/push — доступны status, pr, checks, comments, diff и подтверждённый comment.`,
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
if (!TOOL_ACTIONS.has(action)) {
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
action,
|
|
500
|
+
code: "unknown-action",
|
|
501
|
+
text: `github_review: неизвестное действие «${action}». Доступно: status, pr, checks, comments, diff, comment.`,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const target = prepareTarget({ args, cwd });
|
|
505
|
+
if (!target.ok) return failureResult(action, target.failure);
|
|
506
|
+
if (action === "pr") return prAction(target);
|
|
507
|
+
if (action === "checks") return checksAction(target);
|
|
508
|
+
if (action === "comments") return commentsAction(target);
|
|
509
|
+
return await diffAction(target, args);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return failureResult(action, {
|
|
512
|
+
code: "internal",
|
|
513
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
export function apply(ctx) {
|
|
519
|
+
const tool = {
|
|
520
|
+
name: "github_review",
|
|
521
|
+
description:
|
|
522
|
+
"Review a GitHub pull request through the authenticated `gh` CLI: status (gh install/auth, repo detection), PR overview, CI checks, review comment threads and a byte-capped diff. Read-only by default; the single mutation `comment` runs only with confirm: true plus an explicit PR number and body. Never merges, closes or force-pushes.",
|
|
523
|
+
parameters: TOOL_PARAMETERS,
|
|
524
|
+
timeoutMs: 60_000,
|
|
525
|
+
output: {
|
|
526
|
+
schema: { type: "object", additionalProperties: true },
|
|
527
|
+
render(_args, value) {
|
|
528
|
+
const text = typeof value?.text === "string" ? value.text : JSON.stringify(value);
|
|
529
|
+
return [{ type: "text", text: truncate(text, value?.action === "diff" ? DIFF_RENDER_CHARS : RENDER_CHARS) }];
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
execute: runTool,
|
|
533
|
+
};
|
|
534
|
+
ctx.tools.register(tool);
|
|
535
|
+
ctx.logger?.info?.("[github-review] tool ready");
|
|
536
|
+
}
|