@nghyane/arcane 0.1.25 → 0.1.27
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 +31 -0
- package/package.json +1 -1
- package/src/exec/bash-executor.ts +8 -8
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/selector-controller.ts +1 -4
- package/src/prompts/system/system-prompt.md +11 -2
- package/src/sdk.ts +0 -10
- package/src/session/session-index.ts +63 -36
- package/src/tools/bash-interactive.ts +23 -9
- package/src/tools/bash-interceptor.ts +0 -1
- package/src/tools/bash-normalize.ts +0 -71
- package/src/tools/bash-skill-urls.ts +0 -20
- package/src/tools/bash.ts +4 -20
- package/src/tools/create-tools.ts +2 -4
- package/src/tools/explore.ts +1 -1
- package/src/tools/fetch.ts +13 -19
- package/src/tools/github.ts +130 -28
- package/src/tools/index.ts +2 -5
- package/src/tools/librarian.ts +1 -1
- package/src/tools/oracle.ts +1 -1
- package/src/tools/render-mermaid.ts +61 -5
- package/src/tools/{reviewer-tool.ts → reviewer.ts} +1 -1
- package/src/web/github-client.ts +17 -9
- package/src/tools/gemini-image.ts +0 -905
- package/src/tools/save-memory.ts +0 -185
- /package/src/tools/{subagent-tool.ts → subagent.ts} +0 -0
|
@@ -23,11 +23,10 @@ import { PythonTool } from "./python";
|
|
|
23
23
|
import { ReadTool } from "./read";
|
|
24
24
|
import { ReadThreadTool } from "./read-thread";
|
|
25
25
|
import { RenderMermaidTool } from "./render-mermaid";
|
|
26
|
-
import { reviewerConfig } from "./reviewer
|
|
27
|
-
import { SaveMemoryTool } from "./save-memory";
|
|
26
|
+
import { reviewerConfig } from "./reviewer";
|
|
28
27
|
import { SearchCodeTool } from "./search-code";
|
|
29
28
|
import { loadSshTool } from "./ssh";
|
|
30
|
-
import { SubagentTool } from "./subagent
|
|
29
|
+
import { SubagentTool } from "./subagent";
|
|
31
30
|
import { TodoWriteTool } from "./todo-write";
|
|
32
31
|
import { UndoEditTool } from "./undo-edit";
|
|
33
32
|
import { WriteTool } from "./write";
|
|
@@ -60,7 +59,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
60
59
|
fetch: s => new FetchTool(s),
|
|
61
60
|
web_search: () => new SearchTool(),
|
|
62
61
|
search_code: () => new SearchCodeTool(),
|
|
63
|
-
save_memory: s => new SaveMemoryTool(s),
|
|
64
62
|
write: s => new WriteTool(s),
|
|
65
63
|
};
|
|
66
64
|
|
package/src/tools/explore.ts
CHANGED
package/src/tools/fetch.ts
CHANGED
|
@@ -399,25 +399,15 @@ async function renderHtmlToText(
|
|
|
399
399
|
signal,
|
|
400
400
|
};
|
|
401
401
|
|
|
402
|
-
// Try jina first (reader API)
|
|
403
402
|
try {
|
|
404
|
-
const
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
signal,
|
|
408
|
-
});
|
|
409
|
-
if (response.ok) {
|
|
410
|
-
const content = await response.text();
|
|
411
|
-
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
412
|
-
return { content, ok: true, method: "jina" };
|
|
413
|
-
}
|
|
403
|
+
const content = await htmlToMarkdown(html, { cleanContent: true });
|
|
404
|
+
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
405
|
+
return { content, ok: true, method: "native" };
|
|
414
406
|
}
|
|
415
407
|
} catch {
|
|
416
|
-
// Jina failed, continue to next method
|
|
417
408
|
signal?.throwIfAborted();
|
|
418
409
|
}
|
|
419
410
|
|
|
420
|
-
// Try trafilatura (auto-install via uv/pip)
|
|
421
411
|
const trafilatura = await ensureTool("trafilatura", { signal, silent: true });
|
|
422
412
|
if (trafilatura) {
|
|
423
413
|
const result = await ptree.exec([trafilatura, "-u", url, "--output-format", "markdown"], execOptions);
|
|
@@ -426,7 +416,6 @@ async function renderHtmlToText(
|
|
|
426
416
|
}
|
|
427
417
|
}
|
|
428
418
|
|
|
429
|
-
// Try lynx (can't auto-install, system package)
|
|
430
419
|
const lynx = hasCommand("lynx");
|
|
431
420
|
if (lynx) {
|
|
432
421
|
const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
|
|
@@ -435,14 +424,19 @@ async function renderHtmlToText(
|
|
|
435
424
|
}
|
|
436
425
|
}
|
|
437
426
|
|
|
438
|
-
// Fall back to native converter (fastest, no network/subprocess)
|
|
439
427
|
try {
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
428
|
+
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
429
|
+
const response = await fetch(jinaUrl, {
|
|
430
|
+
headers: { Accept: "text/markdown" },
|
|
431
|
+
signal,
|
|
432
|
+
});
|
|
433
|
+
if (response.ok) {
|
|
434
|
+
const content = await response.text();
|
|
435
|
+
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
436
|
+
return { content, ok: true, method: "jina" };
|
|
437
|
+
}
|
|
443
438
|
}
|
|
444
439
|
} catch {
|
|
445
|
-
// Native converter failed, continue to next method
|
|
446
440
|
signal?.throwIfAborted();
|
|
447
441
|
}
|
|
448
442
|
return { content: "", ok: false, method: "none" };
|
package/src/tools/github.ts
CHANGED
|
@@ -6,9 +6,107 @@ import type { ToolSession } from ".";
|
|
|
6
6
|
import { type OutputMeta, toolResult } from "./output-meta";
|
|
7
7
|
|
|
8
8
|
// =============================================================================
|
|
9
|
-
//
|
|
9
|
+
// GitHub API Types
|
|
10
10
|
// =============================================================================
|
|
11
11
|
|
|
12
|
+
interface GitHubUser {
|
|
13
|
+
login: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface GitHubLabel {
|
|
17
|
+
name: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface GitHubLicense {
|
|
21
|
+
spdx_id: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface GitHubRepo {
|
|
25
|
+
full_name: string;
|
|
26
|
+
description: string | null;
|
|
27
|
+
default_branch: string;
|
|
28
|
+
language: string | null;
|
|
29
|
+
stargazers_count: number;
|
|
30
|
+
forks_count: number;
|
|
31
|
+
topics: string[];
|
|
32
|
+
license: GitHubLicense | null;
|
|
33
|
+
created_at: string;
|
|
34
|
+
updated_at: string;
|
|
35
|
+
homepage: string | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface GitHubTreeEntry {
|
|
39
|
+
type: string;
|
|
40
|
+
path?: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
size?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface GitHubIssue {
|
|
46
|
+
number: number;
|
|
47
|
+
title: string;
|
|
48
|
+
state: string;
|
|
49
|
+
user: GitHubUser | null;
|
|
50
|
+
labels: GitHubLabel[];
|
|
51
|
+
assignees: GitHubUser[];
|
|
52
|
+
body: string | null;
|
|
53
|
+
created_at: string;
|
|
54
|
+
pull_request?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface GitHubComment {
|
|
58
|
+
user: GitHubUser | null;
|
|
59
|
+
created_at: string;
|
|
60
|
+
body: string | null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface GitHubPR {
|
|
64
|
+
number: number;
|
|
65
|
+
title: string;
|
|
66
|
+
state: string;
|
|
67
|
+
merged: boolean;
|
|
68
|
+
merged_at: string | null;
|
|
69
|
+
user: GitHubUser | null;
|
|
70
|
+
base: { ref: string };
|
|
71
|
+
head: { ref: string };
|
|
72
|
+
changed_files: number | null;
|
|
73
|
+
additions: number;
|
|
74
|
+
deletions: number;
|
|
75
|
+
body: string | null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface GitHubCommitAuthor {
|
|
79
|
+
name: string;
|
|
80
|
+
email: string;
|
|
81
|
+
date: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface GitHubCommitFile {
|
|
85
|
+
status: string;
|
|
86
|
+
filename: string;
|
|
87
|
+
additions: number;
|
|
88
|
+
deletions: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface GitHubCommit {
|
|
92
|
+
sha: string;
|
|
93
|
+
commit: {
|
|
94
|
+
author: GitHubCommitAuthor;
|
|
95
|
+
message: string;
|
|
96
|
+
};
|
|
97
|
+
author: GitHubUser | null;
|
|
98
|
+
stats?: { total: number; additions: number; deletions: number };
|
|
99
|
+
files?: GitHubCommitFile[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface GitHubSearchResult<T> {
|
|
103
|
+
total_count: number;
|
|
104
|
+
items: T[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Schema
|
|
109
|
+
// =============================================================================
|
|
12
110
|
const ActionEnum = Type.Union([
|
|
13
111
|
Type.Literal("get_repo"),
|
|
14
112
|
Type.Literal("get_file"),
|
|
@@ -55,7 +153,7 @@ export interface GitHubToolDetails {
|
|
|
55
153
|
// Response Formatters
|
|
56
154
|
// =============================================================================
|
|
57
155
|
|
|
58
|
-
function formatRepo(data:
|
|
156
|
+
function formatRepo(data: GitHubRepo): string {
|
|
59
157
|
return [
|
|
60
158
|
`# ${data.full_name}`,
|
|
61
159
|
data.description ? `${data.description}` : "",
|
|
@@ -72,7 +170,7 @@ function formatRepo(data: any): string {
|
|
|
72
170
|
.join("\n");
|
|
73
171
|
}
|
|
74
172
|
|
|
75
|
-
function formatTreeEntry(entry:
|
|
173
|
+
function formatTreeEntry(entry: GitHubTreeEntry): string {
|
|
76
174
|
const icon = entry.type === "dir" || entry.type === "tree" ? "dir" : "file";
|
|
77
175
|
const size = entry.size ? ` (${formatSize(entry.size)})` : "";
|
|
78
176
|
const name = entry.path ?? entry.name;
|
|
@@ -85,12 +183,12 @@ function formatSize(bytes: number): string {
|
|
|
85
183
|
return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
|
|
86
184
|
}
|
|
87
185
|
|
|
88
|
-
function formatIssue(data:
|
|
186
|
+
function formatIssue(data: GitHubIssue, comments: GitHubComment[] = []): string {
|
|
89
187
|
const lines = [
|
|
90
188
|
`# #${data.number}: ${data.title}`,
|
|
91
189
|
`State: ${data.state} | Author: @${data.user?.login} | Created: ${data.created_at}`,
|
|
92
|
-
data.labels?.length ? `Labels: ${data.labels.map(
|
|
93
|
-
data.assignees?.length ? `Assignees: ${data.assignees.map(
|
|
190
|
+
data.labels?.length ? `Labels: ${data.labels.map(l => l.name).join(", ")}` : "",
|
|
191
|
+
data.assignees?.length ? `Assignees: ${data.assignees.map(a => `@${a.login}`).join(", ")}` : "",
|
|
94
192
|
"",
|
|
95
193
|
data.body ?? "(no description)",
|
|
96
194
|
].filter(l => l !== "");
|
|
@@ -102,12 +200,12 @@ function formatIssue(data: any, comments: any[] = []): string {
|
|
|
102
200
|
return lines.join("\n");
|
|
103
201
|
}
|
|
104
202
|
|
|
105
|
-
function formatIssueMinimal(issue:
|
|
106
|
-
const labels = issue.labels?.length ? ` [${issue.labels.map(
|
|
203
|
+
function formatIssueMinimal(issue: GitHubIssue): string {
|
|
204
|
+
const labels = issue.labels?.length ? ` [${issue.labels.map(l => l.name).join(", ")}]` : "";
|
|
107
205
|
return `#${issue.number} [${issue.state}] ${issue.title}${labels} (@${issue.user?.login}, ${issue.created_at})`;
|
|
108
206
|
}
|
|
109
207
|
|
|
110
|
-
function formatPR(data:
|
|
208
|
+
function formatPR(data: GitHubPR, diff?: string): string {
|
|
111
209
|
const lines = [
|
|
112
210
|
`# PR #${data.number}: ${data.title}`,
|
|
113
211
|
`State: ${data.state}${data.merged ? " (merged)" : ""} | Author: @${data.user?.login}`,
|
|
@@ -124,12 +222,12 @@ function formatPR(data: any, diff?: string): string {
|
|
|
124
222
|
return lines.join("\n");
|
|
125
223
|
}
|
|
126
224
|
|
|
127
|
-
function formatPRMinimal(pr:
|
|
225
|
+
function formatPRMinimal(pr: GitHubPR): string {
|
|
128
226
|
const merged = pr.merged_at ? " (merged)" : "";
|
|
129
227
|
return `#${pr.number} [${pr.state}${merged}] ${pr.title} (@${pr.user?.login}, ${pr.base?.ref} <- ${pr.head?.ref})`;
|
|
130
228
|
}
|
|
131
229
|
|
|
132
|
-
function formatCommit(data:
|
|
230
|
+
function formatCommit(data: GitHubCommit, diff?: string): string {
|
|
133
231
|
const lines = [
|
|
134
232
|
`Commit: ${data.sha}`,
|
|
135
233
|
`Author: ${data.commit?.author?.name} <${data.commit?.author?.email}>`,
|
|
@@ -156,7 +254,7 @@ function formatCommit(data: any, diff?: string): string {
|
|
|
156
254
|
return lines.join("\n");
|
|
157
255
|
}
|
|
158
256
|
|
|
159
|
-
function formatCommitMinimal(c:
|
|
257
|
+
function formatCommitMinimal(c: GitHubCommit): string {
|
|
160
258
|
const sha = (c.sha ?? "").slice(0, 7);
|
|
161
259
|
const msg = (c.commit?.message ?? "").split("\n")[0];
|
|
162
260
|
const author = c.commit?.author?.name ?? c.author?.login ?? "?";
|
|
@@ -164,7 +262,7 @@ function formatCommitMinimal(c: any): string {
|
|
|
164
262
|
return `${sha} ${msg} (${author}, ${date})`;
|
|
165
263
|
}
|
|
166
264
|
|
|
167
|
-
function formatSearchReposResult(data:
|
|
265
|
+
function formatSearchReposResult(data: GitHubSearchResult<GitHubRepo>): string {
|
|
168
266
|
const items = data.items ?? [];
|
|
169
267
|
const lines = [`Found ${data.total_count} repositories (showing ${items.length}):`, ""];
|
|
170
268
|
for (const item of items) {
|
|
@@ -188,7 +286,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
188
286
|
|
|
189
287
|
switch (action) {
|
|
190
288
|
case "get_repo": {
|
|
191
|
-
const res = await githubClient.request(base, opts);
|
|
289
|
+
const res = await githubClient.request<GitHubRepo>(base, opts);
|
|
192
290
|
if (!res.ok) return error(res, "repository");
|
|
193
291
|
return { text: formatRepo(res.data), url: `https://github.com/${owner}/${repo}` };
|
|
194
292
|
}
|
|
@@ -216,7 +314,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
216
314
|
blobRes.data.encoding === "base64"
|
|
217
315
|
? Buffer.from(blobRes.data.content, "base64").toString("utf-8")
|
|
218
316
|
: blobRes.data.content;
|
|
219
|
-
res = { data: decoded as
|
|
317
|
+
res = { data: decoded as string, ok: true, status: 200 };
|
|
220
318
|
}
|
|
221
319
|
}
|
|
222
320
|
}
|
|
@@ -236,10 +334,13 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
236
334
|
const treePath = input.path ?? "";
|
|
237
335
|
if (input.recursive) {
|
|
238
336
|
const ref = input.ref ?? "HEAD";
|
|
239
|
-
const res = await githubClient.request<
|
|
337
|
+
const res = await githubClient.request<{ tree: GitHubTreeEntry[]; truncated: boolean }>(
|
|
338
|
+
`${base}/git/trees/${ref}?recursive=1`,
|
|
339
|
+
opts,
|
|
340
|
+
);
|
|
240
341
|
if (!res.ok) return error(res, "tree");
|
|
241
342
|
const entries = (res.data.tree ?? [])
|
|
242
|
-
.filter(
|
|
343
|
+
.filter(e => !treePath || (e.path ?? "").startsWith(treePath))
|
|
243
344
|
.slice(0, 500);
|
|
244
345
|
return {
|
|
245
346
|
text: `# Tree: ${owner}/${repo}${treePath ? `/${treePath}` : ""} (recursive)\n\n${entries.map(formatTreeEntry).join("\n")}`,
|
|
@@ -247,7 +348,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
247
348
|
}
|
|
248
349
|
const ref = input.ref ? `?ref=${input.ref}` : "";
|
|
249
350
|
const endpoint = treePath ? `${base}/contents/${treePath}${ref}` : `${base}/contents${ref}`;
|
|
250
|
-
const res = await githubClient.request<
|
|
351
|
+
const res = await githubClient.request<GitHubTreeEntry[]>(endpoint, opts);
|
|
251
352
|
if (!res.ok) return error(res, "directory");
|
|
252
353
|
const entries = Array.isArray(res.data) ? res.data : [res.data];
|
|
253
354
|
return {
|
|
@@ -258,7 +359,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
258
359
|
case "search_repos": {
|
|
259
360
|
const q = input.query ?? `${owner}/${repo}`;
|
|
260
361
|
const perPage = Math.min(input.limit ?? 30, 100);
|
|
261
|
-
const res = await githubClient.request<
|
|
362
|
+
const res = await githubClient.request<GitHubSearchResult<GitHubRepo>>(
|
|
262
363
|
`/search/repositories?q=${encodeURIComponent(q)}&per_page=${perPage}`,
|
|
263
364
|
opts,
|
|
264
365
|
);
|
|
@@ -270,8 +371,8 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
270
371
|
const num = input.number;
|
|
271
372
|
if (!num) return { text: "Error: 'number' is required for get_issue" };
|
|
272
373
|
const [issueRes, commentsRes] = await Promise.all([
|
|
273
|
-
githubClient.request<
|
|
274
|
-
githubClient.requestPaginated<
|
|
374
|
+
githubClient.request<GitHubIssue>(`${base}/issues/${num}`, opts),
|
|
375
|
+
githubClient.requestPaginated<GitHubComment>(`${base}/issues/${num}/comments`, {
|
|
275
376
|
...opts,
|
|
276
377
|
perPage: 100,
|
|
277
378
|
maxPages: MAX_COMMENTS_PAGES,
|
|
@@ -291,13 +392,13 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
291
392
|
const limit = Math.min(input.limit ?? 100, 500);
|
|
292
393
|
const perPage = Math.min(limit, 100);
|
|
293
394
|
const maxPages = Math.ceil(limit / perPage);
|
|
294
|
-
const res = await githubClient.requestPaginated<
|
|
395
|
+
const res = await githubClient.requestPaginated<GitHubIssue>(`${base}/issues?${params}`, {
|
|
295
396
|
...opts,
|
|
296
397
|
perPage,
|
|
297
398
|
maxPages,
|
|
298
399
|
});
|
|
299
400
|
if (!res.ok) return error(res, "issues");
|
|
300
|
-
const issues = (res.data ?? []).filter(
|
|
401
|
+
const issues = (res.data ?? []).filter(i => !i.pull_request).slice(0, limit);
|
|
301
402
|
const header = `${issues.length} issue(s)${issues.length >= limit ? " (limit reached, increase limit for more)" : ""}`;
|
|
302
403
|
return {
|
|
303
404
|
text: issues.length ? `${header}\n${issues.map(formatIssueMinimal).join("\n")}` : "No issues found.",
|
|
@@ -307,7 +408,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
307
408
|
case "get_pull": {
|
|
308
409
|
const num = input.number;
|
|
309
410
|
if (!num) return { text: "Error: 'number' is required for get_pull" };
|
|
310
|
-
const prRes = await githubClient.request<
|
|
411
|
+
const prRes = await githubClient.request<GitHubPR>(`${base}/pulls/${num}`, opts);
|
|
311
412
|
if (!prRes.ok) return error(prRes, `PR #${num}`);
|
|
312
413
|
|
|
313
414
|
let diff: string | undefined;
|
|
@@ -336,7 +437,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
336
437
|
const limit = Math.min(input.limit ?? 100, 500);
|
|
337
438
|
const perPage = Math.min(limit, 100);
|
|
338
439
|
const maxPages = Math.ceil(limit / perPage);
|
|
339
|
-
const res = await githubClient.requestPaginated<
|
|
440
|
+
const res = await githubClient.requestPaginated<GitHubPR>(`${base}/pulls?${params}`, {
|
|
340
441
|
...opts,
|
|
341
442
|
perPage,
|
|
342
443
|
maxPages,
|
|
@@ -356,7 +457,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
356
457
|
const limit = Math.min(input.limit ?? 100, 500);
|
|
357
458
|
const perPage = Math.min(limit, 100);
|
|
358
459
|
const maxPages = Math.ceil(limit / perPage);
|
|
359
|
-
const res = await githubClient.requestPaginated<
|
|
460
|
+
const res = await githubClient.requestPaginated<GitHubCommit>(`${base}/commits?${params}`, {
|
|
360
461
|
...opts,
|
|
361
462
|
perPage,
|
|
362
463
|
maxPages,
|
|
@@ -372,7 +473,7 @@ async function handleAction(input: GitHubInput, signal?: AbortSignal): Promise<{
|
|
|
372
473
|
case "get_commit": {
|
|
373
474
|
const sha = input.sha;
|
|
374
475
|
if (!sha) return { text: "Error: 'sha' is required for get_commit" };
|
|
375
|
-
const res = await githubClient.request<
|
|
476
|
+
const res = await githubClient.request<GitHubCommit>(`${base}/commits/${sha}`, opts);
|
|
376
477
|
if (!res.ok) return error(res, `commit ${sha}`);
|
|
377
478
|
|
|
378
479
|
let diff: string | undefined;
|
|
@@ -423,7 +524,8 @@ export class GitHubTool implements AgentTool<typeof schema, GitHubToolDetails, T
|
|
|
423
524
|
readonly name = "github";
|
|
424
525
|
readonly label = "GitHub";
|
|
425
526
|
readonly parameters = schema;
|
|
426
|
-
description =
|
|
527
|
+
description =
|
|
528
|
+
"Interact with GitHub API: repos, issues, PRs, commits. For remote repositories only — use read/grep for local files.";
|
|
427
529
|
|
|
428
530
|
constructor(readonly _session: ToolSession) {}
|
|
429
531
|
|
package/src/tools/index.ts
CHANGED
|
@@ -59,7 +59,6 @@ export {
|
|
|
59
59
|
BashTool,
|
|
60
60
|
type BashToolDetails,
|
|
61
61
|
type BashToolInput,
|
|
62
|
-
type BashToolOptions,
|
|
63
62
|
} from "./bash";
|
|
64
63
|
export { BrowserTool, type BrowserToolDetails } from "./browser";
|
|
65
64
|
export { exploreConfig } from "./explore";
|
|
@@ -72,7 +71,6 @@ export {
|
|
|
72
71
|
type FindToolOptions,
|
|
73
72
|
} from "./find";
|
|
74
73
|
export { FindThreadTool, type FindThreadToolDetails } from "./find-thread";
|
|
75
|
-
export { setPreferredImageProvider } from "./gemini-image";
|
|
76
74
|
export { GitHubTool, type GitHubToolDetails } from "./github";
|
|
77
75
|
export { GrepTool, type GrepToolDetails, type GrepToolInput } from "./grep";
|
|
78
76
|
export { librarianConfig } from "./librarian";
|
|
@@ -86,10 +84,9 @@ export {
|
|
|
86
84
|
export { ReadTool, type ReadToolDetails, type ReadToolInput } from "./read";
|
|
87
85
|
export { ReadThreadTool, type ReadThreadToolDetails } from "./read-thread";
|
|
88
86
|
export { RenderMermaidTool, type RenderMermaidToolDetails } from "./render-mermaid";
|
|
89
|
-
export { reviewerConfig } from "./reviewer
|
|
90
|
-
export { SaveMemoryTool, type SaveMemoryToolDetails } from "./save-memory";
|
|
87
|
+
export { reviewerConfig } from "./reviewer";
|
|
91
88
|
export { loadSshTool, type SSHToolDetails, SshTool } from "./ssh";
|
|
92
|
-
export { type SubagentConfig, SubagentTool } from "./subagent
|
|
89
|
+
export { type SubagentConfig, SubagentTool } from "./subagent";
|
|
93
90
|
export {
|
|
94
91
|
type TodoItem,
|
|
95
92
|
TodoWriteTool,
|
package/src/tools/librarian.ts
CHANGED
package/src/tools/oracle.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import type { SubagentConfig } from "./subagent
|
|
2
|
+
import type { SubagentConfig } from "./subagent";
|
|
3
3
|
|
|
4
4
|
const schema = Type.Object({
|
|
5
5
|
task: Type.String({ description: "Question or task for the oracle to reason about" }),
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@nghyane/arcane-agent";
|
|
2
|
+
import type { Component } from "@nghyane/arcane-tui";
|
|
3
|
+
import { Text } from "@nghyane/arcane-tui";
|
|
2
4
|
import { type AsciiRenderOptions, renderMermaidAscii } from "@nghyane/arcane-utils";
|
|
3
5
|
import { type Static, Type } from "@sinclair/typebox";
|
|
6
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
7
|
+
import type { Theme } from "../theme/theme";
|
|
8
|
+
import { renderStatusLine } from "../tui";
|
|
9
|
+
import { PREVIEW_LIMITS, replaceTabs, TRUNCATE_LENGTHS, truncateToWidth } from "../ui/render-utils";
|
|
4
10
|
import type { ToolSession } from "./index";
|
|
5
11
|
import { allocateOutputArtifact } from "./output-utils";
|
|
6
12
|
|
|
@@ -30,14 +36,15 @@ function sanitizeRenderConfig(config: AsciiRenderOptions | undefined): AsciiRend
|
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
export interface RenderMermaidToolDetails {
|
|
39
|
+
ascii?: string;
|
|
33
40
|
artifactId?: string;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
|
-
export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
|
|
43
|
+
export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails, Theme> {
|
|
37
44
|
readonly name = "render_mermaid";
|
|
38
45
|
readonly label = "RenderMermaid";
|
|
39
46
|
readonly description =
|
|
40
|
-
"Convert Mermaid graph source into ASCII diagram output. Returns ASCII diagram text. Saves full output to an artifact URL when artifact storage is available.";
|
|
47
|
+
"Convert Mermaid graph source into ASCII diagram output. Returns ASCII diagram text. Saves full output to an artifact URL when artifact storage is available. The diagram is displayed directly in the output — do not reproduce it.";
|
|
41
48
|
readonly parameters = renderMermaidSchema;
|
|
42
49
|
readonly strict = true;
|
|
43
50
|
|
|
@@ -55,10 +62,59 @@ export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema,
|
|
|
55
62
|
await Bun.write(artifactPath, ascii);
|
|
56
63
|
}
|
|
57
64
|
|
|
58
|
-
const artifactLine = artifactId ? `\n\nSaved artifact: artifact://${artifactId}` : "";
|
|
59
65
|
return {
|
|
60
|
-
content: [
|
|
61
|
-
|
|
66
|
+
content: [
|
|
67
|
+
{ type: "text", text: artifactId ? `Saved artifact: artifact://${artifactId}` : "Diagram rendered." },
|
|
68
|
+
],
|
|
69
|
+
details: { ascii, artifactId },
|
|
62
70
|
};
|
|
63
71
|
}
|
|
72
|
+
|
|
73
|
+
renderCall(_args: RenderMermaidParams, options: RenderResultOptions, uiTheme: Theme): Component {
|
|
74
|
+
const text = renderStatusLine(
|
|
75
|
+
{
|
|
76
|
+
icon: "running",
|
|
77
|
+
spinnerFrame: options.spinnerFrame,
|
|
78
|
+
title: "RenderMermaid",
|
|
79
|
+
description: "Rendering diagram…",
|
|
80
|
+
},
|
|
81
|
+
uiTheme,
|
|
82
|
+
);
|
|
83
|
+
return new Text(text, 0, 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
renderResult(
|
|
87
|
+
result: {
|
|
88
|
+
content: Array<{ type: string; text?: string }>;
|
|
89
|
+
details?: RenderMermaidToolDetails;
|
|
90
|
+
isError?: boolean;
|
|
91
|
+
},
|
|
92
|
+
options: RenderResultOptions,
|
|
93
|
+
uiTheme: Theme,
|
|
94
|
+
_args?: RenderMermaidParams,
|
|
95
|
+
): Component {
|
|
96
|
+
const icon = result.isError ? "error" : "success";
|
|
97
|
+
const ascii = result.details?.ascii ?? "";
|
|
98
|
+
const outputLines = ascii ? ascii.split("\n") : [];
|
|
99
|
+
const lines: string[] = [];
|
|
100
|
+
|
|
101
|
+
const meta: string[] = [];
|
|
102
|
+
if (outputLines.length > 0) meta.push(`${outputLines.length} lines`);
|
|
103
|
+
if (result.details?.artifactId) meta.push(`artifact://${result.details.artifactId}`);
|
|
104
|
+
|
|
105
|
+
lines.push(renderStatusLine({ icon, title: "RenderMermaid", meta }, uiTheme));
|
|
106
|
+
|
|
107
|
+
if (outputLines.length > 0) {
|
|
108
|
+
const maxLines = options.expanded ? PREVIEW_LIMITS.OUTPUT_EXPANDED : PREVIEW_LIMITS.OUTPUT_COLLAPSED;
|
|
109
|
+
const displayLines = outputLines.slice(0, maxLines);
|
|
110
|
+
for (const line of displayLines) {
|
|
111
|
+
lines.push(uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(line), TRUNCATE_LENGTHS.CONTENT)));
|
|
112
|
+
}
|
|
113
|
+
if (outputLines.length > maxLines) {
|
|
114
|
+
lines.push(uiTheme.fg("dim", `… (${outputLines.length - maxLines} more lines)`));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
119
|
+
}
|
|
64
120
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
|
-
import type { SubagentConfig } from "./subagent
|
|
2
|
+
import type { SubagentConfig } from "./subagent";
|
|
3
3
|
|
|
4
4
|
const schema = Type.Object({
|
|
5
5
|
diff_description: Type.String({ description: "Description of the diff or change to review" }),
|
package/src/web/github-client.ts
CHANGED
|
@@ -76,8 +76,8 @@ const etagCache = new Map<string, CacheEntry>();
|
|
|
76
76
|
const CACHE_MAX_SIZE = 200;
|
|
77
77
|
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
78
78
|
|
|
79
|
-
function getCacheKey(endpoint: string): string {
|
|
80
|
-
return endpoint;
|
|
79
|
+
function getCacheKey(endpoint: string, mediaType?: string): string {
|
|
80
|
+
return mediaType ? `${endpoint}::${mediaType}` : endpoint;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function pruneCache(): void {
|
|
@@ -122,8 +122,7 @@ async function request<T = unknown>(endpoint: string, options: RequestOptions =
|
|
|
122
122
|
headers.Accept = options.mediaType;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
const cacheKey = getCacheKey(endpoint);
|
|
125
|
+
const cacheKey = getCacheKey(endpoint, options.mediaType);
|
|
127
126
|
const cached = etagCache.get(cacheKey);
|
|
128
127
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
129
128
|
headers["If-None-Match"] = cached.etag;
|
|
@@ -177,11 +176,20 @@ async function request<T = unknown>(endpoint: string, options: RequestOptions =
|
|
|
177
176
|
}
|
|
178
177
|
|
|
179
178
|
// Retry on transient errors
|
|
180
|
-
|
|
179
|
+
const isRateLimited =
|
|
180
|
+
response.status === 429 ||
|
|
181
|
+
(response.status === 403 && (rateLimit.remaining === 0 || response.headers.has("retry-after")));
|
|
182
|
+
const isTransient = RETRY_STATUS_CODES.has(response.status) || isRateLimited;
|
|
183
|
+
|
|
184
|
+
if (isTransient && attempt < MAX_RETRIES) {
|
|
185
|
+
await response.text().catch(() => {});
|
|
181
186
|
const retryAfter = response.headers.get("retry-after");
|
|
182
|
-
const
|
|
187
|
+
const retrySeconds = retryAfter ? parseInt(retryAfter, 10) : Number.NaN;
|
|
188
|
+
const waitMs = Number.isFinite(retrySeconds)
|
|
189
|
+
? retrySeconds * 1000
|
|
190
|
+
: Math.min(1000 * 2 ** attempt, 10_000);
|
|
183
191
|
|
|
184
|
-
if (
|
|
192
|
+
if (isRateLimited && rateLimit.remaining === 0) {
|
|
185
193
|
const resetMs = rateLimit.reset * 1000 - Date.now();
|
|
186
194
|
if (resetMs > 30_000) {
|
|
187
195
|
return { data: null as T, ok: false, status: response.status, rateLimit };
|
|
@@ -197,8 +205,8 @@ async function request<T = unknown>(endpoint: string, options: RequestOptions =
|
|
|
197
205
|
return { data: null as T, ok: false, status: response.status, rateLimit };
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
const
|
|
201
|
-
const data = (
|
|
208
|
+
const wantsRaw = options.mediaType !== undefined;
|
|
209
|
+
const data = (wantsRaw ? await response.text() : await response.json()) as T;
|
|
202
210
|
|
|
203
211
|
// Cache with ETag
|
|
204
212
|
const etag = response.headers.get("etag");
|