@nghyane/arcane 0.1.16 → 0.1.18
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 +21 -0
- package/package.json +7 -15
- package/src/cli/setup-cli.ts +2 -62
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +1 -4
- package/src/config/settings-schema.ts +23 -98
- package/src/config/settings.ts +0 -1
- package/src/exa/mcp-client.ts +57 -2
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/custom-tools/wrapper.ts +1 -1
- package/src/extensibility/extensions/wrapper.ts +1 -1
- package/src/extensibility/hooks/tool-wrapper.ts +1 -1
- package/src/internal-urls/docs-index.generated.ts +1 -2
- package/src/internal-urls/index.ts +2 -4
- package/src/internal-urls/router.ts +2 -2
- package/src/internal-urls/types.ts +2 -2
- package/src/mcp/oauth-flow.ts +1 -1
- package/src/modes/components/custom-editor.ts +6 -2
- package/src/modes/controllers/command-controller.ts +4 -46
- package/src/modes/controllers/input-controller.ts +123 -6
- package/src/modes/interactive-mode.ts +1 -84
- package/src/modes/types.ts +0 -1
- package/src/patch/edit-tool.ts +2 -11
- package/src/patch/hashline.ts +42 -0
- package/src/prompts/agents/explore.md +4 -2
- package/src/prompts/agents/librarian.md +4 -6
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +5 -1
- package/src/prompts/system/system-prompt.md +29 -18
- package/src/prompts/thread-extract.md +16 -0
- package/src/prompts/tools/render-mermaid.md +9 -0
- package/src/sdk.ts +12 -37
- package/src/session/agent-session.ts +5 -10
- package/src/session/retry-utils.ts +1 -1
- package/src/session/session-index.ts +329 -0
- package/src/session/session-manager.ts +0 -30
- package/src/session/streaming-edit.ts +1 -36
- package/src/slash-commands/builtin-registry.ts +0 -16
- package/src/task/index.ts +1 -1
- package/src/tools/ask.ts +9 -6
- package/src/tools/bash-skill-urls.ts +3 -3
- package/src/tools/bash.ts +2 -1
- package/src/tools/create-tools.ts +28 -33
- package/src/tools/fetch.ts +1 -1
- package/src/tools/find-thread.ts +120 -0
- package/src/tools/grep.ts +2 -1
- package/src/tools/index.ts +5 -0
- package/src/tools/python.ts +53 -1
- package/src/tools/read-thread.ts +409 -0
- package/src/tools/read.ts +4 -3
- package/src/tools/render-mermaid.ts +68 -0
- package/src/tools/save-memory.ts +182 -0
- package/src/tools/write.ts +1 -1
- package/src/web/search/index.ts +4 -1
- package/src/web/search/provider.ts +3 -0
- package/src/web/search/providers/anthropic.ts +1 -0
- package/src/web/search/providers/gemini.ts +122 -37
- package/src/web/search/providers/kagi.ts +163 -0
- package/src/web/search/types.ts +1 -0
- package/src/internal-urls/memory-protocol.ts +0 -133
- package/src/memories/index.ts +0 -1099
- package/src/memories/storage.ts +0 -563
- package/src/patch/normative.ts +0 -72
- package/src/prompts/memories/consolidation.md +0 -30
- package/src/prompts/memories/read_path.md +0 -11
- package/src/prompts/memories/stage_one_input.md +0 -6
- package/src/prompts/memories/stage_one_system.md +0 -21
- package/src/stt/downloader.ts +0 -68
- package/src/stt/index.ts +0 -3
- package/src/stt/recorder.ts +0 -351
- package/src/stt/setup.ts +0 -50
- package/src/stt/stt-controller.ts +0 -160
- package/src/stt/transcribe.py +0 -70
- package/src/stt/transcriber.ts +0 -91
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { Database, type Statement } from "bun:sqlite";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { logger, parseJsonlLenient } from "@nghyane/arcane-utils";
|
|
5
|
+
import { getAgentDir } from "@nghyane/arcane-utils/dirs";
|
|
6
|
+
|
|
7
|
+
export interface SessionIndexEntry {
|
|
8
|
+
sessionId: string;
|
|
9
|
+
title: string;
|
|
10
|
+
firstMessage: string;
|
|
11
|
+
files: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
createdAt: number;
|
|
14
|
+
messageCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SessionSearchResult {
|
|
18
|
+
threadId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
date: string;
|
|
21
|
+
messageCount: number;
|
|
22
|
+
snippet: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface SearchRow {
|
|
26
|
+
session_id: string;
|
|
27
|
+
title: string;
|
|
28
|
+
created_at: number;
|
|
29
|
+
message_count: number;
|
|
30
|
+
snippet: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const FILE_PATH_PARAMS = new Set(["path", "file", "filePath", "glob", "pattern", "command_working_directory"]);
|
|
34
|
+
|
|
35
|
+
export class SessionIndex {
|
|
36
|
+
#db: Database;
|
|
37
|
+
static #instance?: SessionIndex;
|
|
38
|
+
|
|
39
|
+
#upsertStmt: Statement;
|
|
40
|
+
#hasStmt: Statement;
|
|
41
|
+
|
|
42
|
+
private constructor(dbPath: string) {
|
|
43
|
+
const dir = path.dirname(dbPath);
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
|
|
46
|
+
this.#db = new Database(dbPath);
|
|
47
|
+
|
|
48
|
+
this.#db.exec(`
|
|
49
|
+
PRAGMA journal_mode=WAL;
|
|
50
|
+
PRAGMA synchronous=NORMAL;
|
|
51
|
+
PRAGMA busy_timeout=5000;
|
|
52
|
+
|
|
53
|
+
CREATE TABLE IF NOT EXISTS session_index (
|
|
54
|
+
session_id TEXT PRIMARY KEY,
|
|
55
|
+
title TEXT,
|
|
56
|
+
first_message TEXT,
|
|
57
|
+
files TEXT,
|
|
58
|
+
cwd TEXT,
|
|
59
|
+
created_at INTEGER,
|
|
60
|
+
message_count INTEGER
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS session_fts USING fts5(
|
|
64
|
+
title, first_message, files,
|
|
65
|
+
content='session_index', content_rowid='rowid'
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
CREATE TRIGGER IF NOT EXISTS session_index_ai AFTER INSERT ON session_index BEGIN
|
|
69
|
+
INSERT INTO session_fts(rowid, title, first_message, files)
|
|
70
|
+
VALUES (new.rowid, new.title, new.first_message, new.files);
|
|
71
|
+
END;
|
|
72
|
+
|
|
73
|
+
CREATE TRIGGER IF NOT EXISTS session_index_ad AFTER DELETE ON session_index BEGIN
|
|
74
|
+
INSERT INTO session_fts(session_fts, rowid, title, first_message, files)
|
|
75
|
+
VALUES ('delete', old.rowid, old.title, old.first_message, old.files);
|
|
76
|
+
END;
|
|
77
|
+
|
|
78
|
+
CREATE TRIGGER IF NOT EXISTS session_index_au AFTER UPDATE ON session_index BEGIN
|
|
79
|
+
INSERT INTO session_fts(session_fts, rowid, title, first_message, files)
|
|
80
|
+
VALUES ('delete', old.rowid, old.title, old.first_message, old.files);
|
|
81
|
+
INSERT INTO session_fts(rowid, title, first_message, files)
|
|
82
|
+
VALUES (new.rowid, new.title, new.first_message, new.files);
|
|
83
|
+
END;
|
|
84
|
+
`);
|
|
85
|
+
|
|
86
|
+
this.#upsertStmt = this.#db.prepare(
|
|
87
|
+
"INSERT OR REPLACE INTO session_index (session_id, title, first_message, files, cwd, created_at, message_count) VALUES (?, ?, ?, ?, ?, ?, ?)",
|
|
88
|
+
);
|
|
89
|
+
this.#hasStmt = this.#db.prepare("SELECT 1 FROM session_index WHERE session_id = ?");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static open(dbPath: string = path.join(getAgentDir(), "session-index.db")): SessionIndex {
|
|
93
|
+
if (!SessionIndex.#instance) {
|
|
94
|
+
SessionIndex.#instance = new SessionIndex(dbPath);
|
|
95
|
+
}
|
|
96
|
+
return SessionIndex.#instance;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
upsert(entry: SessionIndexEntry): void {
|
|
100
|
+
try {
|
|
101
|
+
this.#upsertStmt.run(
|
|
102
|
+
entry.sessionId,
|
|
103
|
+
entry.title,
|
|
104
|
+
entry.firstMessage,
|
|
105
|
+
entry.files,
|
|
106
|
+
entry.cwd,
|
|
107
|
+
entry.createdAt,
|
|
108
|
+
entry.messageCount,
|
|
109
|
+
);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logger.error("SessionIndex upsert failed", { error: String(error) });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
has(sessionId: string): boolean {
|
|
116
|
+
return this.#hasStmt.get(sessionId) != null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
search(query: string, limit: number): SessionSearchResult[] {
|
|
120
|
+
const safeLimit = Math.min(Math.max(1, Math.floor(limit)), 50);
|
|
121
|
+
const { ftsQuery, afterTs, beforeTs } = this.#parseQuery(query);
|
|
122
|
+
if (!ftsQuery) return [];
|
|
123
|
+
|
|
124
|
+
const conditions = ["session_fts MATCH ?"];
|
|
125
|
+
const params: (string | number)[] = [ftsQuery];
|
|
126
|
+
|
|
127
|
+
if (afterTs != null) {
|
|
128
|
+
conditions.push("si.created_at >= ?");
|
|
129
|
+
params.push(afterTs);
|
|
130
|
+
}
|
|
131
|
+
if (beforeTs != null) {
|
|
132
|
+
conditions.push("si.created_at <= ?");
|
|
133
|
+
params.push(beforeTs);
|
|
134
|
+
}
|
|
135
|
+
params.push(safeLimit);
|
|
136
|
+
|
|
137
|
+
const sql = `SELECT si.session_id, si.title, si.created_at, si.message_count,
|
|
138
|
+
snippet(session_fts, -1, '<match>', '</match>', '...', 32) as snippet
|
|
139
|
+
FROM session_fts f
|
|
140
|
+
JOIN session_index si ON si.rowid = f.rowid
|
|
141
|
+
WHERE ${conditions.join(" AND ")}
|
|
142
|
+
ORDER BY si.created_at DESC
|
|
143
|
+
LIMIT ?`;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const rows = this.#db.prepare(sql).all(...params) as SearchRow[];
|
|
147
|
+
return rows.map(row => ({
|
|
148
|
+
threadId: row.session_id,
|
|
149
|
+
title: row.title || "Untitled",
|
|
150
|
+
date: new Date(row.created_at * 1000).toISOString().slice(0, 10),
|
|
151
|
+
messageCount: row.message_count,
|
|
152
|
+
snippet: row.snippet || "",
|
|
153
|
+
}));
|
|
154
|
+
} catch (error) {
|
|
155
|
+
logger.error("SessionIndex search failed", { error: String(error) });
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async indexSessionFile(filePath: string): Promise<void> {
|
|
161
|
+
try {
|
|
162
|
+
const content = await Bun.file(filePath).text();
|
|
163
|
+
const entries = parseJsonlLenient<Record<string, unknown>>(content);
|
|
164
|
+
if (entries.length === 0) return;
|
|
165
|
+
|
|
166
|
+
const header = entries.find(e => e.type === "session") as
|
|
167
|
+
| { type: string; id?: string; title?: string; cwd?: string; timestamp?: string }
|
|
168
|
+
| undefined;
|
|
169
|
+
if (!header?.id) return;
|
|
170
|
+
|
|
171
|
+
let firstMessage = "";
|
|
172
|
+
let messageCount = 0;
|
|
173
|
+
const fileSet = new Set<string>();
|
|
174
|
+
|
|
175
|
+
for (const entry of entries) {
|
|
176
|
+
if (entry.type !== "message") continue;
|
|
177
|
+
const msg = entry.message as { role?: string; content?: unknown } | undefined;
|
|
178
|
+
if (!msg?.role) continue;
|
|
179
|
+
|
|
180
|
+
messageCount++;
|
|
181
|
+
|
|
182
|
+
if (msg.role === "user" && !firstMessage) {
|
|
183
|
+
firstMessage = extractTextContent(msg.content);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (msg.role === "assistant") {
|
|
187
|
+
extractFilePaths(msg.content, fileSet);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const title = header.title || firstMessage.slice(0, 100) || "Untitled";
|
|
192
|
+
const createdAt = header.timestamp ? Math.floor(new Date(header.timestamp).getTime() / 1000) : 0;
|
|
193
|
+
|
|
194
|
+
this.upsert({
|
|
195
|
+
sessionId: header.id,
|
|
196
|
+
title,
|
|
197
|
+
firstMessage: firstMessage.slice(0, 500),
|
|
198
|
+
files: [...fileSet].join(" "),
|
|
199
|
+
cwd: header.cwd || "",
|
|
200
|
+
createdAt,
|
|
201
|
+
messageCount,
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.warn("SessionIndex indexSessionFile failed", { path: filePath, error: String(error) });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async indexAllSessions(sessionsDir?: string): Promise<void> {
|
|
209
|
+
const dir = sessionsDir ?? path.join(getAgentDir(), "sessions");
|
|
210
|
+
let subdirs: string[];
|
|
211
|
+
try {
|
|
212
|
+
subdirs = fs.readdirSync(dir);
|
|
213
|
+
} catch {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
for (const subdir of subdirs) {
|
|
218
|
+
const subdirPath = path.join(dir, subdir);
|
|
219
|
+
let stat: fs.Stats;
|
|
220
|
+
try {
|
|
221
|
+
stat = fs.statSync(subdirPath);
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (!stat.isDirectory()) continue;
|
|
226
|
+
|
|
227
|
+
let files: string[];
|
|
228
|
+
try {
|
|
229
|
+
files = fs.readdirSync(subdirPath).filter(f => f.endsWith(".jsonl"));
|
|
230
|
+
} catch {
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const file of files) {
|
|
235
|
+
const filePath = path.join(subdirPath, file);
|
|
236
|
+
try {
|
|
237
|
+
const firstLine = await Bun.file(filePath).text();
|
|
238
|
+
const headerLine = firstLine.split("\n")[0];
|
|
239
|
+
if (!headerLine) continue;
|
|
240
|
+
const header = JSON.parse(headerLine) as { id?: string };
|
|
241
|
+
if (!header.id) continue;
|
|
242
|
+
if (this.has(header.id)) continue;
|
|
243
|
+
await this.indexSessionFile(filePath);
|
|
244
|
+
} catch {}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
#parseQuery(query: string): { ftsQuery: string | null; afterTs: number | null; beforeTs: number | null } {
|
|
250
|
+
let afterTs: number | null = null;
|
|
251
|
+
let beforeTs: number | null = null;
|
|
252
|
+
|
|
253
|
+
const remaining = query.replace(/\b(after|before):(\S+)/g, (_, dir: string, val: string) => {
|
|
254
|
+
const ts = this.#parseDate(val);
|
|
255
|
+
if (ts != null) {
|
|
256
|
+
if (dir === "after") afterTs = ts;
|
|
257
|
+
else beforeTs = ts;
|
|
258
|
+
}
|
|
259
|
+
return "";
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const ftsQuery = this.#buildFtsQuery(remaining);
|
|
263
|
+
return { ftsQuery, afterTs, beforeTs };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#parseDate(value: string): number | null {
|
|
267
|
+
const relMatch = value.match(/^(\d+)([dwm])$/);
|
|
268
|
+
if (relMatch) {
|
|
269
|
+
const n = Number.parseInt(relMatch[1], 10);
|
|
270
|
+
const unit = relMatch[2];
|
|
271
|
+
const now = Date.now();
|
|
272
|
+
let ms = 0;
|
|
273
|
+
if (unit === "d") ms = n * 86400_000;
|
|
274
|
+
else if (unit === "w") ms = n * 7 * 86400_000;
|
|
275
|
+
else if (unit === "m") ms = n * 30 * 86400_000;
|
|
276
|
+
return Math.floor((now - ms) / 1000);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const d = new Date(value);
|
|
280
|
+
if (!Number.isNaN(d.getTime())) {
|
|
281
|
+
return Math.floor(d.getTime() / 1000);
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#buildFtsQuery(query: string): string | null {
|
|
287
|
+
const tokens = query
|
|
288
|
+
.trim()
|
|
289
|
+
.split(/\s+/)
|
|
290
|
+
.map(t => t.trim())
|
|
291
|
+
.filter(Boolean);
|
|
292
|
+
|
|
293
|
+
if (tokens.length === 0) return null;
|
|
294
|
+
|
|
295
|
+
return tokens
|
|
296
|
+
.map(token => {
|
|
297
|
+
const escaped = token.replace(/"/g, '""');
|
|
298
|
+
return `"${escaped}"*`;
|
|
299
|
+
})
|
|
300
|
+
.join(" ");
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function extractTextContent(content: unknown): string {
|
|
305
|
+
if (typeof content === "string") return content;
|
|
306
|
+
if (Array.isArray(content)) {
|
|
307
|
+
for (const block of content) {
|
|
308
|
+
if (block && typeof block === "object" && "type" in block && block.type === "text" && "text" in block) {
|
|
309
|
+
return String(block.text);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return "";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function extractFilePaths(content: unknown, fileSet: Set<string>): void {
|
|
317
|
+
if (!Array.isArray(content)) return;
|
|
318
|
+
for (const block of content) {
|
|
319
|
+
if (!block || typeof block !== "object") continue;
|
|
320
|
+
if (!("type" in block) || block.type !== "toolCall") continue;
|
|
321
|
+
const args = "arguments" in block ? (block.arguments as Record<string, unknown>) : null;
|
|
322
|
+
if (!args) continue;
|
|
323
|
+
for (const [key, val] of Object.entries(args)) {
|
|
324
|
+
if (FILE_PATH_PARAMS.has(key) && typeof val === "string" && val.length > 0) {
|
|
325
|
+
fileSet.add(val);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -1581,36 +1581,6 @@ export class SessionManager {
|
|
|
1581
1581
|
await this.#rewriteFile();
|
|
1582
1582
|
}
|
|
1583
1583
|
|
|
1584
|
-
/**
|
|
1585
|
-
* Rewrite tool call arguments in the most recent assistant message containing the toolCallId.
|
|
1586
|
-
* Returns true if a tool call was updated.
|
|
1587
|
-
*/
|
|
1588
|
-
async rewriteAssistantToolCallArgs(toolCallId: string, args: Record<string, unknown>): Promise<boolean> {
|
|
1589
|
-
let updated = false;
|
|
1590
|
-
for (let i = this.#fileEntries.length - 1; i >= 0; i--) {
|
|
1591
|
-
const entry = this.#fileEntries[i];
|
|
1592
|
-
if (entry.type !== "message" || entry.message.role !== "assistant") continue;
|
|
1593
|
-
const message = entry.message as { content?: unknown };
|
|
1594
|
-
if (!Array.isArray(message.content)) continue;
|
|
1595
|
-
for (const block of message.content) {
|
|
1596
|
-
if (typeof block !== "object" || block === null) continue;
|
|
1597
|
-
if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
|
|
1598
|
-
const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
|
|
1599
|
-
if (toolCall.id === toolCallId) {
|
|
1600
|
-
toolCall.arguments = args;
|
|
1601
|
-
updated = true;
|
|
1602
|
-
break;
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
if (updated) break;
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
if (updated && this.persist && this.#sessionFile) {
|
|
1609
|
-
await this.#rewriteFile();
|
|
1610
|
-
}
|
|
1611
|
-
return updated;
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
1584
|
/**
|
|
1615
1585
|
* Append a custom message entry (for extensions) that participates in LLM context.
|
|
1616
1586
|
* @param customType Hook identifier for filtering on reload
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
|
|
3
3
|
import type { Agent, AgentEvent } from "@nghyane/arcane-agent";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ToolCall } from "@nghyane/arcane-ai";
|
|
5
5
|
import { isEnoent, logger } from "@nghyane/arcane-utils";
|
|
6
6
|
import type { Settings } from "../config/settings";
|
|
7
7
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
8
8
|
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
9
9
|
import { resolveToCwd } from "../tools/path-utils";
|
|
10
|
-
import type { SessionManager } from "./session-manager";
|
|
11
10
|
|
|
12
11
|
/**
|
|
13
12
|
* Mutable state for streaming edit abort detection.
|
|
@@ -222,37 +221,3 @@ async function checkPreviewPatchAsync(
|
|
|
222
221
|
agent.abort();
|
|
223
222
|
}
|
|
224
223
|
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Rewrite tool call arguments in agent state and persisted session history.
|
|
228
|
-
*/
|
|
229
|
-
export async function rewriteToolCallArgs(
|
|
230
|
-
agent: Agent,
|
|
231
|
-
sessionManager: SessionManager,
|
|
232
|
-
toolCallId: string,
|
|
233
|
-
args: Record<string, unknown>,
|
|
234
|
-
): Promise<void> {
|
|
235
|
-
let updated = false;
|
|
236
|
-
const messages = agent.state.messages;
|
|
237
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
238
|
-
const msg = messages[i];
|
|
239
|
-
if (msg.role !== "assistant") continue;
|
|
240
|
-
const assistantMsg = msg as AssistantMessage;
|
|
241
|
-
if (!Array.isArray(assistantMsg.content)) continue;
|
|
242
|
-
for (const block of assistantMsg.content) {
|
|
243
|
-
if (typeof block !== "object" || block === null) continue;
|
|
244
|
-
if (!("type" in block) || (block as { type?: string }).type !== "toolCall") continue;
|
|
245
|
-
const toolCall = block as { id?: string; arguments?: Record<string, unknown> };
|
|
246
|
-
if (toolCall.id === toolCallId) {
|
|
247
|
-
toolCall.arguments = args;
|
|
248
|
-
updated = true;
|
|
249
|
-
break;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (updated) break;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if (updated) {
|
|
256
|
-
await sessionManager.rewriteAssistantToolCallArgs(toolCallId, args);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
@@ -349,22 +349,6 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
349
349
|
runtime.ctx.editor.setText("");
|
|
350
350
|
},
|
|
351
351
|
},
|
|
352
|
-
{
|
|
353
|
-
name: "memory",
|
|
354
|
-
description: "Inspect and operate memory maintenance",
|
|
355
|
-
subcommands: [
|
|
356
|
-
{ name: "view", description: "Show current memory injection payload" },
|
|
357
|
-
{ name: "clear", description: "Clear persisted memory data and artifacts" },
|
|
358
|
-
{ name: "reset", description: "Alias for clear" },
|
|
359
|
-
{ name: "enqueue", description: "Enqueue memory consolidation maintenance" },
|
|
360
|
-
{ name: "rebuild", description: "Alias for enqueue" },
|
|
361
|
-
],
|
|
362
|
-
allowArgs: true,
|
|
363
|
-
handle: async (command, runtime) => {
|
|
364
|
-
runtime.ctx.editor.setText("");
|
|
365
|
-
await runtime.ctx.handleMemoryCommand(command.text);
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
352
|
{
|
|
369
353
|
name: "move",
|
|
370
354
|
description: "Move session to a different working directory",
|
package/src/task/index.ts
CHANGED
|
@@ -189,7 +189,7 @@ export class TaskTool implements AgentTool<TaskSchema, TaskToolDetails, Theme> {
|
|
|
189
189
|
modelRegistry: this.session.subagentContext?.modelRegistry,
|
|
190
190
|
settings: this.session.settings,
|
|
191
191
|
mcpManager: this.session.subagentContext?.mcpManager,
|
|
192
|
-
contextFiles: this.session.contextFiles,
|
|
192
|
+
contextFiles: this.session.contextFiles?.filter(f => !f.path.endsWith("AGENTS.md")),
|
|
193
193
|
skills: this.session.skills,
|
|
194
194
|
promptTemplates: this.session.promptTemplates,
|
|
195
195
|
});
|
package/src/tools/ask.ts
CHANGED
|
@@ -111,14 +111,15 @@ interface UIContext {
|
|
|
111
111
|
select(
|
|
112
112
|
prompt: string,
|
|
113
113
|
options: string[],
|
|
114
|
-
options_?: { initialIndex?: number; timeout?: number; outline?: boolean },
|
|
114
|
+
options_?: { initialIndex?: number; timeout?: number; outline?: boolean; signal?: AbortSignal },
|
|
115
115
|
): Promise<string | undefined>;
|
|
116
|
-
input(prompt: string): Promise<string | undefined>;
|
|
116
|
+
input(prompt: string, placeholder?: string, options_?: { signal?: AbortSignal }): Promise<string | undefined>;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
interface AskQuestionOptions {
|
|
120
120
|
/** Timeout in milliseconds, null/undefined to disable */
|
|
121
121
|
timeout?: number | null;
|
|
122
|
+
signal?: AbortSignal;
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
async function askSingleQuestion(
|
|
@@ -158,6 +159,7 @@ async function askSingleQuestion(
|
|
|
158
159
|
initialIndex: cursorIndex,
|
|
159
160
|
timeout: timeout ?? undefined,
|
|
160
161
|
outline: true,
|
|
162
|
+
signal: options?.signal,
|
|
161
163
|
});
|
|
162
164
|
const elapsed = Date.now() - selectionStart;
|
|
163
165
|
const timedOut = timeout != null && elapsed >= timeout;
|
|
@@ -166,7 +168,7 @@ async function askSingleQuestion(
|
|
|
166
168
|
|
|
167
169
|
if (choice === OTHER_OPTION) {
|
|
168
170
|
if (!timedOut) {
|
|
169
|
-
const input = await ui.input("Enter your response:");
|
|
171
|
+
const input = await ui.input("Enter your response:", undefined, { signal: options?.signal });
|
|
170
172
|
if (input) customInput = input;
|
|
171
173
|
}
|
|
172
174
|
break;
|
|
@@ -205,9 +207,10 @@ async function askSingleQuestion(
|
|
|
205
207
|
timeout: timeout ?? undefined,
|
|
206
208
|
initialIndex: recommended,
|
|
207
209
|
outline: true,
|
|
210
|
+
signal: options?.signal,
|
|
208
211
|
});
|
|
209
212
|
if (choice === OTHER_OPTION) {
|
|
210
|
-
const input = await ui.input("Enter your response:");
|
|
213
|
+
const input = await ui.input("Enter your response:", undefined, { signal: options?.signal });
|
|
211
214
|
if (input) customInput = input;
|
|
212
215
|
} else if (choice) {
|
|
213
216
|
selectedOptions = [stripRecommendedSuffix(choice)];
|
|
@@ -300,7 +303,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails, Them
|
|
|
300
303
|
optionLabels,
|
|
301
304
|
q.multi ?? false,
|
|
302
305
|
q.recommended,
|
|
303
|
-
{ timeout },
|
|
306
|
+
{ timeout, signal: _signal },
|
|
304
307
|
);
|
|
305
308
|
|
|
306
309
|
const details: AskToolDetails = {
|
|
@@ -335,7 +338,7 @@ export class AskTool implements AgentTool<typeof askSchema, AskToolDetails, Them
|
|
|
335
338
|
optionLabels,
|
|
336
339
|
q.multi ?? false,
|
|
337
340
|
q.recommended,
|
|
338
|
-
{ timeout },
|
|
341
|
+
{ timeout, signal: _signal },
|
|
339
342
|
);
|
|
340
343
|
|
|
341
344
|
results.push({
|
|
@@ -9,9 +9,9 @@ const SKILL_URL_PATTERN = /'skill:\/\/[^'\s")`\\]+'|"skill:\/\/[^"\s')`\\]+"|ski
|
|
|
9
9
|
|
|
10
10
|
/** Regex to find supported internal URL tokens in command text. */
|
|
11
11
|
const INTERNAL_URL_PATTERN =
|
|
12
|
-
/'(?:skill|agent|artifact|plan|
|
|
12
|
+
/'(?:skill|agent|artifact|plan|rule):\/\/[^'\s")`\\]+'|"(?:skill|agent|artifact|plan|rule):\/\/[^"\s')`\\]+"|(?:skill|agent|artifact|plan|rule):\/\/[^\s'")`\\]+/g;
|
|
13
13
|
|
|
14
|
-
const SUPPORTED_INTERNAL_SCHEMES = ["skill", "agent", "artifact", "plan", "
|
|
14
|
+
const SUPPORTED_INTERNAL_SCHEMES = ["skill", "agent", "artifact", "plan", "rule"] as const;
|
|
15
15
|
|
|
16
16
|
type SupportedInternalScheme = (typeof SUPPORTED_INTERNAL_SCHEMES)[number];
|
|
17
17
|
|
|
@@ -152,7 +152,7 @@ export function expandSkillUrls(command: string, skills: readonly Skill[]): stri
|
|
|
152
152
|
|
|
153
153
|
/**
|
|
154
154
|
* Expand supported internal URLs in a bash command string to shell-escaped absolute paths.
|
|
155
|
-
* Supported schemes: skill://, agent://, artifact://, plan://,
|
|
155
|
+
* Supported schemes: skill://, agent://, artifact://, plan://, rule://
|
|
156
156
|
*/
|
|
157
157
|
export async function expandInternalUrls(command: string, options: InternalUrlExpansionOptions): Promise<string> {
|
|
158
158
|
if (!command.includes("://")) return command;
|
package/src/tools/bash.ts
CHANGED
|
@@ -55,7 +55,8 @@ function isInteractiveResult(result: BashResult | BashInteractiveResult): result
|
|
|
55
55
|
export class BashTool implements AgentTool<typeof bashSchema, BashToolDetails, Theme> {
|
|
56
56
|
readonly name = "bash";
|
|
57
57
|
readonly label = "Bash";
|
|
58
|
-
description =
|
|
58
|
+
description =
|
|
59
|
+
"Execute a shell command. Use grep/find instead of shell grep/find, read instead of cat/head/tail, edit instead of sed/awk, write instead of echo/printf redirects.";
|
|
59
60
|
readonly parameters = bashSchema;
|
|
60
61
|
readonly concurrency = "exclusive";
|
|
61
62
|
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { AgentTool } from "@nghyane/arcane-agent";
|
|
2
|
-
import { $env
|
|
3
|
-
import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
|
|
4
|
-
import { checkPythonKernelAvailability } from "../ipy/kernel";
|
|
2
|
+
import { $env } from "@nghyane/arcane-utils";
|
|
5
3
|
import { LspTool } from "../lsp";
|
|
6
4
|
import { EditTool } from "../patch";
|
|
7
5
|
import { TaskTool } from "../task";
|
|
@@ -13,6 +11,7 @@ import { BrowserTool } from "./browser";
|
|
|
13
11
|
import { exploreConfig } from "./explore";
|
|
14
12
|
import { FetchTool } from "./fetch";
|
|
15
13
|
import { FindTool } from "./find";
|
|
14
|
+
import { FindThreadTool } from "./find-thread";
|
|
16
15
|
import { GitHubTool } from "./github";
|
|
17
16
|
import { GrepTool } from "./grep";
|
|
18
17
|
import type { ToolSession } from "./index";
|
|
@@ -22,7 +21,10 @@ import { oracleConfig } from "./oracle";
|
|
|
22
21
|
import { wrapToolWithMetaNotice } from "./output-meta";
|
|
23
22
|
import { PythonTool } from "./python";
|
|
24
23
|
import { ReadTool } from "./read";
|
|
24
|
+
import { ReadThreadTool } from "./read-thread";
|
|
25
|
+
import { RenderMermaidTool } from "./render-mermaid";
|
|
25
26
|
import { reviewerConfig } from "./reviewer-tool";
|
|
27
|
+
import { SaveMemoryTool } from "./save-memory";
|
|
26
28
|
import { SearchCodeTool } from "./search-code";
|
|
27
29
|
import { loadSshTool } from "./ssh";
|
|
28
30
|
import { SubagentTool } from "./subagent-tool";
|
|
@@ -38,6 +40,8 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
38
40
|
python: s => new PythonTool(s),
|
|
39
41
|
ssh: loadSshTool,
|
|
40
42
|
edit: s => new EditTool(s),
|
|
43
|
+
find_thread: s => new FindThreadTool(s),
|
|
44
|
+
read_thread: s => new ReadThreadTool(s),
|
|
41
45
|
find: s => new FindTool(s),
|
|
42
46
|
explore: s => new SubagentTool(s, exploreConfig),
|
|
43
47
|
github: s => new GitHubTool(s),
|
|
@@ -47,6 +51,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
47
51
|
notebook: s => new NotebookTool(s),
|
|
48
52
|
oracle: s => new SubagentTool(s, oracleConfig),
|
|
49
53
|
read: s => new ReadTool(s),
|
|
54
|
+
render_mermaid: s => new RenderMermaidTool(s),
|
|
50
55
|
browser: s => new BrowserTool(s),
|
|
51
56
|
task: TaskTool.create,
|
|
52
57
|
code_review: s => new SubagentTool(s, reviewerConfig),
|
|
@@ -55,6 +60,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
55
60
|
fetch: s => new FetchTool(s),
|
|
56
61
|
web_search: () => new SearchTool(),
|
|
57
62
|
search_code: () => new SearchCodeTool(),
|
|
63
|
+
save_memory: s => new SaveMemoryTool(s),
|
|
58
64
|
write: s => new WriteTool(s),
|
|
59
65
|
};
|
|
60
66
|
|
|
@@ -98,37 +104,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
98
104
|
const enableLsp = session.enableLsp ?? true;
|
|
99
105
|
const requestedTools = toolNames && toolNames.length > 0 ? [...new Set(toolNames)] : undefined;
|
|
100
106
|
const pythonMode = getPythonModeFromEnv() ?? session.settings.get("python.toolMode");
|
|
101
|
-
const skipPythonPreflight = session.skipPythonPreflight === true;
|
|
102
|
-
let pythonAvailable = true;
|
|
103
|
-
const shouldCheckPython =
|
|
104
|
-
!skipPythonPreflight &&
|
|
105
|
-
pythonMode !== "bash-only" &&
|
|
106
|
-
(requestedTools === undefined || requestedTools.includes("python"));
|
|
107
|
-
const isTestEnv = Bun.env.BUN_ENV === "test" || Bun.env.NODE_ENV === "test";
|
|
108
|
-
const skipPythonWarm = isTestEnv || $env.ARCANE_PYTHON_SKIP_CHECK === "1";
|
|
109
|
-
if (shouldCheckPython) {
|
|
110
|
-
const availability = await checkPythonKernelAvailability(session.cwd);
|
|
111
|
-
time("createTools:pythonCheck");
|
|
112
|
-
pythonAvailable = availability.ok;
|
|
113
|
-
if (!availability.ok) {
|
|
114
|
-
logger.warn("Python kernel unavailable, falling back to bash", {
|
|
115
|
-
reason: availability.reason,
|
|
116
|
-
});
|
|
117
|
-
} else if (!skipPythonWarm && getPreludeDocs().length === 0) {
|
|
118
|
-
const sessionFile = session.getSessionFile?.() ?? undefined;
|
|
119
|
-
const warmSessionId = sessionFile ? `session:${sessionFile}:cwd:${session.cwd}` : `cwd:${session.cwd}`;
|
|
120
|
-
try {
|
|
121
|
-
await warmPythonEnvironment(session.cwd, warmSessionId, session.settings.get("python.sharedGateway"));
|
|
122
|
-
time("createTools:warmPython");
|
|
123
|
-
} catch (err) {
|
|
124
|
-
logger.warn("Failed to warm Python environment", {
|
|
125
|
-
error: err instanceof Error ? err.message : String(err),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
107
|
|
|
131
|
-
const effectiveMode =
|
|
108
|
+
const effectiveMode = pythonMode;
|
|
132
109
|
const allowBash = effectiveMode !== "ipy-only";
|
|
133
110
|
const allowPython = effectiveMode !== "bash-only";
|
|
134
111
|
if (
|
|
@@ -153,6 +130,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
153
130
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
154
131
|
if (name === "lsp") return session.settings.get("lsp.enabled");
|
|
155
132
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
133
|
+
if (name === "render_mermaid") return session.settings.get("renderMermaid.enabled");
|
|
156
134
|
if (name === "librarian") return session.settings.get("librarian.enabled");
|
|
157
135
|
if (name === "oracle") return session.settings.get("oracle.enabled");
|
|
158
136
|
if (name === "github") return session.settings.get("github.enabled");
|
|
@@ -182,5 +160,22 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
182
160
|
);
|
|
183
161
|
const tools = results.filter((r): r is AgentTool => r !== null);
|
|
184
162
|
|
|
163
|
+
// Auto-include AST tools when their text-based counterparts are enabled
|
|
164
|
+
const toolNameSet = new Set(tools.map(t => t.name));
|
|
165
|
+
if (session.settings.get("astGrep.enabled") && toolNameSet.has("grep") && !toolNameSet.has("ast_grep")) {
|
|
166
|
+
const astGrepFactory = allTools.ast_grep;
|
|
167
|
+
if (astGrepFactory) {
|
|
168
|
+
const astGrepTool = await astGrepFactory(session);
|
|
169
|
+
if (astGrepTool) tools.push(wrapToolWithMetaNotice(astGrepTool));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (session.settings.get("astEdit.enabled") && toolNameSet.has("edit") && !toolNameSet.has("ast_edit")) {
|
|
173
|
+
const astEditFactory = allTools.ast_edit;
|
|
174
|
+
if (astEditFactory) {
|
|
175
|
+
const astEditTool = await astEditFactory(session);
|
|
176
|
+
if (astEditTool) tools.push(wrapToolWithMetaNotice(astEditTool));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
185
180
|
return tools;
|
|
186
181
|
}
|
package/src/tools/fetch.ts
CHANGED
|
@@ -850,7 +850,7 @@ export interface FetchToolDetails {
|
|
|
850
850
|
export class FetchTool implements AgentTool<typeof fetchSchema, FetchToolDetails, Theme> {
|
|
851
851
|
readonly name = "fetch";
|
|
852
852
|
readonly label = "Fetch";
|
|
853
|
-
description = "Fetch a URL and return its content";
|
|
853
|
+
description = "Fetch a URL and return its content. Do NOT use for localhost or local URLs; use bash curl instead.";
|
|
854
854
|
readonly parameters = fetchSchema;
|
|
855
855
|
|
|
856
856
|
constructor(private readonly session: ToolSession) {}
|