openplanter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +210 -0
- package/dist/builder.d.ts +11 -0
- package/dist/builder.d.ts.map +1 -0
- package/dist/builder.js +179 -0
- package/dist/builder.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +548 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +114 -0
- package/dist/config.js.map +1 -0
- package/dist/credentials.d.ts +52 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +371 -0
- package/dist/credentials.js.map +1 -0
- package/dist/demo.d.ts +26 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +95 -0
- package/dist/demo.js.map +1 -0
- package/dist/engine.d.ts +91 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1036 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/investigation-tools/aph-holdings.d.ts +61 -0
- package/dist/investigation-tools/aph-holdings.d.ts.map +1 -0
- package/dist/investigation-tools/aph-holdings.js +459 -0
- package/dist/investigation-tools/aph-holdings.js.map +1 -0
- package/dist/investigation-tools/asic-officer-lookup.d.ts +42 -0
- package/dist/investigation-tools/asic-officer-lookup.d.ts.map +1 -0
- package/dist/investigation-tools/asic-officer-lookup.js +197 -0
- package/dist/investigation-tools/asic-officer-lookup.js.map +1 -0
- package/dist/investigation-tools/asx-calendar-fetcher.d.ts +42 -0
- package/dist/investigation-tools/asx-calendar-fetcher.d.ts.map +1 -0
- package/dist/investigation-tools/asx-calendar-fetcher.js +271 -0
- package/dist/investigation-tools/asx-calendar-fetcher.js.map +1 -0
- package/dist/investigation-tools/asx-parser.d.ts +66 -0
- package/dist/investigation-tools/asx-parser.d.ts.map +1 -0
- package/dist/investigation-tools/asx-parser.js +314 -0
- package/dist/investigation-tools/asx-parser.js.map +1 -0
- package/dist/investigation-tools/bulk-asx-announcements.d.ts +53 -0
- package/dist/investigation-tools/bulk-asx-announcements.d.ts.map +1 -0
- package/dist/investigation-tools/bulk-asx-announcements.js +204 -0
- package/dist/investigation-tools/bulk-asx-announcements.js.map +1 -0
- package/dist/investigation-tools/entity-resolver.d.ts +77 -0
- package/dist/investigation-tools/entity-resolver.d.ts.map +1 -0
- package/dist/investigation-tools/entity-resolver.js +346 -0
- package/dist/investigation-tools/entity-resolver.js.map +1 -0
- package/dist/investigation-tools/hotcopper-scraper.d.ts +73 -0
- package/dist/investigation-tools/hotcopper-scraper.d.ts.map +1 -0
- package/dist/investigation-tools/hotcopper-scraper.js +318 -0
- package/dist/investigation-tools/hotcopper-scraper.js.map +1 -0
- package/dist/investigation-tools/index.d.ts +15 -0
- package/dist/investigation-tools/index.d.ts.map +1 -0
- package/dist/investigation-tools/index.js +15 -0
- package/dist/investigation-tools/index.js.map +1 -0
- package/dist/investigation-tools/insider-graph.d.ts +173 -0
- package/dist/investigation-tools/insider-graph.d.ts.map +1 -0
- package/dist/investigation-tools/insider-graph.js +732 -0
- package/dist/investigation-tools/insider-graph.js.map +1 -0
- package/dist/investigation-tools/insider-suspicion-scorer.d.ts +97 -0
- package/dist/investigation-tools/insider-suspicion-scorer.d.ts.map +1 -0
- package/dist/investigation-tools/insider-suspicion-scorer.js +327 -0
- package/dist/investigation-tools/insider-suspicion-scorer.js.map +1 -0
- package/dist/investigation-tools/multi-forum-scraper.d.ts +104 -0
- package/dist/investigation-tools/multi-forum-scraper.d.ts.map +1 -0
- package/dist/investigation-tools/multi-forum-scraper.js +415 -0
- package/dist/investigation-tools/multi-forum-scraper.js.map +1 -0
- package/dist/investigation-tools/price-fetcher.d.ts +81 -0
- package/dist/investigation-tools/price-fetcher.d.ts.map +1 -0
- package/dist/investigation-tools/price-fetcher.js +268 -0
- package/dist/investigation-tools/price-fetcher.js.map +1 -0
- package/dist/investigation-tools/shared.d.ts +39 -0
- package/dist/investigation-tools/shared.d.ts.map +1 -0
- package/dist/investigation-tools/shared.js +203 -0
- package/dist/investigation-tools/shared.js.map +1 -0
- package/dist/investigation-tools/timeline-linker.d.ts +90 -0
- package/dist/investigation-tools/timeline-linker.d.ts.map +1 -0
- package/dist/investigation-tools/timeline-linker.js +219 -0
- package/dist/investigation-tools/timeline-linker.js.map +1 -0
- package/dist/investigation-tools/volume-scanner.d.ts +70 -0
- package/dist/investigation-tools/volume-scanner.d.ts.map +1 -0
- package/dist/investigation-tools/volume-scanner.js +227 -0
- package/dist/investigation-tools/volume-scanner.js.map +1 -0
- package/dist/model.d.ts +136 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +1071 -0
- package/dist/model.js.map +1 -0
- package/dist/patching.d.ts +45 -0
- package/dist/patching.d.ts.map +1 -0
- package/dist/patching.js +317 -0
- package/dist/patching.js.map +1 -0
- package/dist/prompts.d.ts +15 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +351 -0
- package/dist/prompts.js.map +1 -0
- package/dist/replay-log.d.ts +54 -0
- package/dist/replay-log.d.ts.map +1 -0
- package/dist/replay-log.js +94 -0
- package/dist/replay-log.js.map +1 -0
- package/dist/runtime.d.ts +53 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +259 -0
- package/dist/runtime.js.map +1 -0
- package/dist/settings.d.ts +39 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +146 -0
- package/dist/settings.js.map +1 -0
- package/dist/tool-defs.d.ts +58 -0
- package/dist/tool-defs.d.ts.map +1 -0
- package/dist/tool-defs.js +1029 -0
- package/dist/tool-defs.js.map +1 -0
- package/dist/tools.d.ts +72 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1454 -0
- package/dist/tools.js.map +1 -0
- package/dist/tui.d.ts +49 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +699 -0
- package/dist/tui.js.map +1 -0
- package/package.json +126 -0
package/dist/tools.js
ADDED
|
@@ -0,0 +1,1454 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as zlib from "node:zlib";
|
|
6
|
+
import { spawn, execSync } from "node:child_process";
|
|
7
|
+
import * as http from "node:http";
|
|
8
|
+
import * as https from "node:https";
|
|
9
|
+
import { PatchApplyError, apply_agent_patch, parse_agent_patch, } from "./patching.js";
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Constants
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
const _MAX_WALK_ENTRIES = 50_000;
|
|
14
|
+
const _WS_RE = /\s+/g;
|
|
15
|
+
const _HASHLINE_PREFIX_RE = /^\d+:[0-9a-f]{2}\|/;
|
|
16
|
+
const _HEREDOC_RE = /<<-?\s*['"]?\w+['"]?/;
|
|
17
|
+
const _INTERACTIVE_RE = /(^|[;&|]\s*)(vim|nano|less|more|top|htop|man)\b/;
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
/** 2-char hex hash, whitespace-invariant. */
|
|
22
|
+
function _line_hash(line) {
|
|
23
|
+
const normalized = line.replace(_WS_RE, "");
|
|
24
|
+
const crc = zlib.crc32(Buffer.from(normalized, "utf-8"));
|
|
25
|
+
return (crc & 0xff).toString(16).padStart(2, "0");
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// ToolError
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
export class ToolError extends Error {
|
|
31
|
+
constructor(message) {
|
|
32
|
+
super(message);
|
|
33
|
+
this.name = "ToolError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// WorkspaceTools
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export class WorkspaceTools {
|
|
40
|
+
root;
|
|
41
|
+
shell;
|
|
42
|
+
commandTimeoutSec;
|
|
43
|
+
maxShellOutputChars;
|
|
44
|
+
maxFileChars;
|
|
45
|
+
maxFilesListed;
|
|
46
|
+
maxSearchHits;
|
|
47
|
+
exaApiKey;
|
|
48
|
+
exaBaseUrl;
|
|
49
|
+
_bgJobs = new Map();
|
|
50
|
+
_bgNextId = 1;
|
|
51
|
+
// Runtime policy state
|
|
52
|
+
_filesRead = new Set();
|
|
53
|
+
_parallelWriteClaims = new Map();
|
|
54
|
+
// Scope-local state (simulated via simple fields; Node is single-threaded)
|
|
55
|
+
_scopeGroupId = null;
|
|
56
|
+
_scopeOwnerId = null;
|
|
57
|
+
// ------------------------------------------------------------------
|
|
58
|
+
// Domain tools (tools/ directory CLI scripts)
|
|
59
|
+
// ------------------------------------------------------------------
|
|
60
|
+
static _DOMAIN_TOOL_SCRIPTS = {
|
|
61
|
+
aph_holdings: "aph_holdings.py",
|
|
62
|
+
asx_parser: "asx_parser.py",
|
|
63
|
+
asx_calendar_fetcher: "asx_calendar_fetcher.py",
|
|
64
|
+
bulk_asx_announcements: "bulk_asx_announcements.py",
|
|
65
|
+
asic_officer_lookup: "asic_officer_lookup.py",
|
|
66
|
+
entity_resolver: "entity_resolver.py",
|
|
67
|
+
hotcopper_scraper: "hotcopper_scraper.py",
|
|
68
|
+
insider_graph: "insider_graph.py",
|
|
69
|
+
insider_suspicion_scorer: "insider_suspicion_scorer.py",
|
|
70
|
+
multi_forum_scraper: "multi_forum_scraper.py",
|
|
71
|
+
price_fetcher: "price_fetcher.py",
|
|
72
|
+
timeline_linker: "timeline_linker.py",
|
|
73
|
+
volume_scanner: "volume_scanner.py",
|
|
74
|
+
};
|
|
75
|
+
static _DOMAIN_ARG_MAP = {
|
|
76
|
+
asx_calendar_fetcher: {
|
|
77
|
+
tickers: "--tickers",
|
|
78
|
+
period: "--period",
|
|
79
|
+
format: "--format",
|
|
80
|
+
test: "--test",
|
|
81
|
+
},
|
|
82
|
+
bulk_asx_announcements: {
|
|
83
|
+
tickers: "--tickers",
|
|
84
|
+
types: "--types",
|
|
85
|
+
days_back: "--days-back",
|
|
86
|
+
output_dir: "--output-dir",
|
|
87
|
+
format: "--format",
|
|
88
|
+
test: "--test",
|
|
89
|
+
},
|
|
90
|
+
asic_officer_lookup: {
|
|
91
|
+
abn_or_ticker: "--abn-or-ticker",
|
|
92
|
+
max_results: "--max-results",
|
|
93
|
+
format: "--format",
|
|
94
|
+
test: "--test",
|
|
95
|
+
},
|
|
96
|
+
multi_forum_scraper: {
|
|
97
|
+
ticker: "--ticker",
|
|
98
|
+
sites: "--sites",
|
|
99
|
+
days: "--days",
|
|
100
|
+
keywords: "--keywords",
|
|
101
|
+
format: "--format",
|
|
102
|
+
test: "--test",
|
|
103
|
+
},
|
|
104
|
+
insider_suspicion_scorer: {
|
|
105
|
+
trades: "--trades",
|
|
106
|
+
anomalies: "--anomalies",
|
|
107
|
+
rumors: "--rumors",
|
|
108
|
+
holdings: "--holdings",
|
|
109
|
+
output: "--output",
|
|
110
|
+
min_score: "--min-score",
|
|
111
|
+
format: "--format",
|
|
112
|
+
test: "--test",
|
|
113
|
+
},
|
|
114
|
+
aph_holdings: {
|
|
115
|
+
member: "--member",
|
|
116
|
+
chamber: "--chamber",
|
|
117
|
+
cache_dir: "--cache-dir",
|
|
118
|
+
test: "--test",
|
|
119
|
+
},
|
|
120
|
+
asx_parser: {
|
|
121
|
+
input: "--input",
|
|
122
|
+
type: "--type",
|
|
123
|
+
test: "--test",
|
|
124
|
+
},
|
|
125
|
+
entity_resolver: {
|
|
126
|
+
mode: "--mode",
|
|
127
|
+
input: "--input",
|
|
128
|
+
reference: "--reference",
|
|
129
|
+
threshold: "--threshold",
|
|
130
|
+
test: "--test",
|
|
131
|
+
},
|
|
132
|
+
hotcopper_scraper: {
|
|
133
|
+
ticker: "--ticker",
|
|
134
|
+
days: "--days",
|
|
135
|
+
pages: "--pages",
|
|
136
|
+
format: "--format",
|
|
137
|
+
test: "--test",
|
|
138
|
+
},
|
|
139
|
+
insider_graph: {
|
|
140
|
+
input: "--input",
|
|
141
|
+
mode: "--mode",
|
|
142
|
+
find_path: "--find-path",
|
|
143
|
+
connections: "--connections",
|
|
144
|
+
depth: "--depth",
|
|
145
|
+
clusters: "--clusters",
|
|
146
|
+
suspicion: "--suspicion",
|
|
147
|
+
export_format: "--export-format",
|
|
148
|
+
stats: "--stats",
|
|
149
|
+
test: "--test",
|
|
150
|
+
},
|
|
151
|
+
price_fetcher: {
|
|
152
|
+
tickers: "--tickers",
|
|
153
|
+
period: "--period",
|
|
154
|
+
interval: "--interval",
|
|
155
|
+
format: "--format",
|
|
156
|
+
anomalies_only: "--anomalies-only",
|
|
157
|
+
summary: "--summary",
|
|
158
|
+
test: "--test",
|
|
159
|
+
},
|
|
160
|
+
timeline_linker: {
|
|
161
|
+
trades: "--trades",
|
|
162
|
+
events: "--events",
|
|
163
|
+
window: "--window",
|
|
164
|
+
min_score: "--min-score",
|
|
165
|
+
date_from: "--from",
|
|
166
|
+
date_to: "--to",
|
|
167
|
+
summary: "--summary",
|
|
168
|
+
test: "--test",
|
|
169
|
+
},
|
|
170
|
+
volume_scanner: {
|
|
171
|
+
tickers: "--tickers",
|
|
172
|
+
watchlist: "--watchlist",
|
|
173
|
+
days: "--days",
|
|
174
|
+
threshold: "--threshold",
|
|
175
|
+
format: "--format",
|
|
176
|
+
report_dates: "--report-dates",
|
|
177
|
+
test: "--test",
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
constructor(opts) {
|
|
181
|
+
const resolved = path.resolve(opts.root);
|
|
182
|
+
if (!fs.existsSync(resolved)) {
|
|
183
|
+
throw new ToolError(`Workspace does not exist: ${resolved}`);
|
|
184
|
+
}
|
|
185
|
+
const stat = fs.statSync(resolved);
|
|
186
|
+
if (!stat.isDirectory()) {
|
|
187
|
+
throw new ToolError(`Workspace is not a directory: ${resolved}`);
|
|
188
|
+
}
|
|
189
|
+
this.root = resolved;
|
|
190
|
+
this.shell = opts.shell ?? "/bin/sh";
|
|
191
|
+
this.commandTimeoutSec = opts.commandTimeoutSec ?? 45;
|
|
192
|
+
this.maxShellOutputChars = opts.maxShellOutputChars ?? 16000;
|
|
193
|
+
this.maxFileChars = opts.maxFileChars ?? 20000;
|
|
194
|
+
this.maxFilesListed = opts.maxFilesListed ?? 400;
|
|
195
|
+
this.maxSearchHits = opts.maxSearchHits ?? 200;
|
|
196
|
+
this.exaApiKey = opts.exaApiKey ?? null;
|
|
197
|
+
this.exaBaseUrl = opts.exaBaseUrl ?? "https://api.exa.ai";
|
|
198
|
+
}
|
|
199
|
+
// ------------------------------------------------------------------
|
|
200
|
+
// Private helpers
|
|
201
|
+
// ------------------------------------------------------------------
|
|
202
|
+
_clip(text, maxChars) {
|
|
203
|
+
if (text.length <= maxChars) {
|
|
204
|
+
return text;
|
|
205
|
+
}
|
|
206
|
+
const omitted = text.length - maxChars;
|
|
207
|
+
return `${text.slice(0, maxChars)}\n\n...[truncated ${omitted} chars]...`;
|
|
208
|
+
}
|
|
209
|
+
_resolvePath(rawPath) {
|
|
210
|
+
let candidate;
|
|
211
|
+
if (path.isAbsolute(rawPath)) {
|
|
212
|
+
candidate = rawPath;
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
candidate = path.join(this.root, rawPath);
|
|
216
|
+
}
|
|
217
|
+
const resolved = path.resolve(candidate);
|
|
218
|
+
if (resolved === this.root) {
|
|
219
|
+
return resolved;
|
|
220
|
+
}
|
|
221
|
+
const relative = path.relative(this.root, resolved);
|
|
222
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
223
|
+
throw new ToolError(`Path escapes workspace: ${rawPath}`);
|
|
224
|
+
}
|
|
225
|
+
return resolved;
|
|
226
|
+
}
|
|
227
|
+
_checkShellPolicy(command) {
|
|
228
|
+
if (_HEREDOC_RE.test(command)) {
|
|
229
|
+
return ("BLOCKED: Heredoc syntax (<< EOF) is not allowed by runtime policy. " +
|
|
230
|
+
"Use write_file/apply_patch for multi-line content.");
|
|
231
|
+
}
|
|
232
|
+
if (_INTERACTIVE_RE.test(command)) {
|
|
233
|
+
return ("BLOCKED: Interactive terminal programs are not allowed by runtime policy " +
|
|
234
|
+
"(vim/nano/less/more/top/htop/man).");
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
_registerWriteTarget(resolved) {
|
|
239
|
+
const groupId = this._scopeGroupId;
|
|
240
|
+
const ownerId = this._scopeOwnerId;
|
|
241
|
+
if (!groupId || !ownerId) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
let claims = this._parallelWriteClaims.get(groupId);
|
|
245
|
+
if (!claims) {
|
|
246
|
+
claims = new Map();
|
|
247
|
+
this._parallelWriteClaims.set(groupId, claims);
|
|
248
|
+
}
|
|
249
|
+
const owner = claims.get(resolved);
|
|
250
|
+
if (owner === undefined) {
|
|
251
|
+
claims.set(resolved, ownerId);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (owner !== ownerId) {
|
|
255
|
+
const rel = path.relative(this.root, resolved).split(path.sep).join("/");
|
|
256
|
+
throw new ToolError(`Parallel write conflict: '${rel}' is already claimed by sibling task ${owner}.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// ------------------------------------------------------------------
|
|
260
|
+
// Parallel write group management
|
|
261
|
+
// ------------------------------------------------------------------
|
|
262
|
+
beginParallelWriteGroup(groupId) {
|
|
263
|
+
this._parallelWriteClaims.set(groupId, new Map());
|
|
264
|
+
}
|
|
265
|
+
endParallelWriteGroup(groupId) {
|
|
266
|
+
this._parallelWriteClaims.delete(groupId);
|
|
267
|
+
}
|
|
268
|
+
executionScope(groupId, ownerId, fn) {
|
|
269
|
+
const prevGroup = this._scopeGroupId;
|
|
270
|
+
const prevOwner = this._scopeOwnerId;
|
|
271
|
+
this._scopeGroupId = groupId;
|
|
272
|
+
this._scopeOwnerId = ownerId;
|
|
273
|
+
try {
|
|
274
|
+
return fn();
|
|
275
|
+
}
|
|
276
|
+
finally {
|
|
277
|
+
this._scopeGroupId = prevGroup;
|
|
278
|
+
this._scopeOwnerId = prevOwner;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
async executionScopeAsync(groupId, ownerId, fn) {
|
|
282
|
+
const prevGroup = this._scopeGroupId;
|
|
283
|
+
const prevOwner = this._scopeOwnerId;
|
|
284
|
+
this._scopeGroupId = groupId;
|
|
285
|
+
this._scopeOwnerId = ownerId;
|
|
286
|
+
try {
|
|
287
|
+
return await fn();
|
|
288
|
+
}
|
|
289
|
+
finally {
|
|
290
|
+
this._scopeGroupId = prevGroup;
|
|
291
|
+
this._scopeOwnerId = prevOwner;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// ------------------------------------------------------------------
|
|
295
|
+
// Shell execution
|
|
296
|
+
// ------------------------------------------------------------------
|
|
297
|
+
async runShell(command, timeout) {
|
|
298
|
+
const policyError = this._checkShellPolicy(command);
|
|
299
|
+
if (policyError) {
|
|
300
|
+
return policyError;
|
|
301
|
+
}
|
|
302
|
+
const effectiveTimeout = Math.max(1, Math.min(timeout ?? this.commandTimeoutSec, 600));
|
|
303
|
+
return new Promise((resolve) => {
|
|
304
|
+
let proc;
|
|
305
|
+
try {
|
|
306
|
+
proc = spawn(command, {
|
|
307
|
+
shell: this.shell,
|
|
308
|
+
cwd: this.root,
|
|
309
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
310
|
+
detached: true,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (exc) {
|
|
314
|
+
resolve(`$ ${command}\n[failed to start: ${exc}]`);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
let stdout = "";
|
|
318
|
+
let stderr = "";
|
|
319
|
+
let finished = false;
|
|
320
|
+
proc.stdout?.on("data", (chunk) => {
|
|
321
|
+
stdout += chunk.toString("utf-8");
|
|
322
|
+
});
|
|
323
|
+
proc.stderr?.on("data", (chunk) => {
|
|
324
|
+
stderr += chunk.toString("utf-8");
|
|
325
|
+
});
|
|
326
|
+
const timer = setTimeout(() => {
|
|
327
|
+
if (!finished) {
|
|
328
|
+
finished = true;
|
|
329
|
+
try {
|
|
330
|
+
if (proc.pid !== undefined) {
|
|
331
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
try {
|
|
336
|
+
proc.kill("SIGKILL");
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// ignore
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
resolve(`$ ${command}\n[timeout after ${effectiveTimeout}s — processes killed]`);
|
|
343
|
+
}
|
|
344
|
+
}, effectiveTimeout * 1000);
|
|
345
|
+
proc.on("close", (code) => {
|
|
346
|
+
if (!finished) {
|
|
347
|
+
finished = true;
|
|
348
|
+
clearTimeout(timer);
|
|
349
|
+
const merged = `$ ${command}\n` +
|
|
350
|
+
`[exit_code=${code ?? -1}]\n` +
|
|
351
|
+
`[stdout]\n${stdout}\n` +
|
|
352
|
+
`[stderr]\n${stderr}`;
|
|
353
|
+
resolve(this._clip(merged, this.maxShellOutputChars));
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
proc.on("error", (err) => {
|
|
357
|
+
if (!finished) {
|
|
358
|
+
finished = true;
|
|
359
|
+
clearTimeout(timer);
|
|
360
|
+
resolve(`$ ${command}\n[failed to start: ${err}]`);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
runShellBg(command) {
|
|
366
|
+
const policyError = this._checkShellPolicy(command);
|
|
367
|
+
if (policyError) {
|
|
368
|
+
return policyError;
|
|
369
|
+
}
|
|
370
|
+
const outPath = path.join(os.tmpdir(), `.rlm_bg_${this._bgNextId}.out`);
|
|
371
|
+
const fd = fs.openSync(outPath, "w+");
|
|
372
|
+
let proc;
|
|
373
|
+
try {
|
|
374
|
+
proc = spawn(command, {
|
|
375
|
+
shell: this.shell,
|
|
376
|
+
cwd: this.root,
|
|
377
|
+
stdio: ["ignore", fd, fd],
|
|
378
|
+
detached: true,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
catch (exc) {
|
|
382
|
+
fs.closeSync(fd);
|
|
383
|
+
try {
|
|
384
|
+
fs.unlinkSync(outPath);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
// ignore
|
|
388
|
+
}
|
|
389
|
+
return `Failed to start background command: ${exc}`;
|
|
390
|
+
}
|
|
391
|
+
const jobId = this._bgNextId;
|
|
392
|
+
this._bgNextId += 1;
|
|
393
|
+
this._bgJobs.set(jobId, { proc, outPath, fd });
|
|
394
|
+
return `Background job started: job_id=${jobId}, pid=${proc.pid}`;
|
|
395
|
+
}
|
|
396
|
+
checkShellBg(jobId) {
|
|
397
|
+
const entry = this._bgJobs.get(jobId);
|
|
398
|
+
if (!entry) {
|
|
399
|
+
return `No background job with id ${jobId}`;
|
|
400
|
+
}
|
|
401
|
+
const { proc, outPath, fd } = entry;
|
|
402
|
+
const exitCode = proc.exitCode;
|
|
403
|
+
let output = "";
|
|
404
|
+
try {
|
|
405
|
+
output = fs.readFileSync(outPath, "utf-8");
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// ignore
|
|
409
|
+
}
|
|
410
|
+
output = this._clip(output, this.maxShellOutputChars);
|
|
411
|
+
if (exitCode !== null) {
|
|
412
|
+
fs.closeSync(fd);
|
|
413
|
+
try {
|
|
414
|
+
fs.unlinkSync(outPath);
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
// ignore
|
|
418
|
+
}
|
|
419
|
+
this._bgJobs.delete(jobId);
|
|
420
|
+
return `[job ${jobId} finished, exit_code=${exitCode}]\n${output}`;
|
|
421
|
+
}
|
|
422
|
+
return `[job ${jobId} still running, pid=${proc.pid}]\n${output}`;
|
|
423
|
+
}
|
|
424
|
+
killShellBg(jobId) {
|
|
425
|
+
const entry = this._bgJobs.get(jobId);
|
|
426
|
+
if (!entry) {
|
|
427
|
+
return `No background job with id ${jobId}`;
|
|
428
|
+
}
|
|
429
|
+
const { proc, outPath, fd } = entry;
|
|
430
|
+
try {
|
|
431
|
+
if (proc.pid !== undefined) {
|
|
432
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
try {
|
|
437
|
+
proc.kill("SIGKILL");
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// ignore
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
fs.closeSync(fd);
|
|
444
|
+
try {
|
|
445
|
+
fs.unlinkSync(outPath);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
// ignore
|
|
449
|
+
}
|
|
450
|
+
this._bgJobs.delete(jobId);
|
|
451
|
+
return `Background job ${jobId} killed.`;
|
|
452
|
+
}
|
|
453
|
+
cleanupBgJobs() {
|
|
454
|
+
for (const [jobId, { proc, outPath, fd }] of this._bgJobs) {
|
|
455
|
+
try {
|
|
456
|
+
if (proc.pid !== undefined) {
|
|
457
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
try {
|
|
462
|
+
proc.kill("SIGKILL");
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
// ignore
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
fs.closeSync(fd);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// ignore
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
fs.unlinkSync(outPath);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// ignore
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
this._bgJobs.clear();
|
|
482
|
+
}
|
|
483
|
+
// ------------------------------------------------------------------
|
|
484
|
+
// File listing & search
|
|
485
|
+
// ------------------------------------------------------------------
|
|
486
|
+
async listFiles(glob) {
|
|
487
|
+
let lines;
|
|
488
|
+
if (this._hasRg()) {
|
|
489
|
+
const cmd = ["rg", "--files", "--hidden", "-g", "!.git"];
|
|
490
|
+
if (glob) {
|
|
491
|
+
cmd.push("-g", glob);
|
|
492
|
+
}
|
|
493
|
+
const result = await this._spawnCapture(cmd, this.root, this.commandTimeoutSec);
|
|
494
|
+
if (result === null) {
|
|
495
|
+
return "(list_files timed out)";
|
|
496
|
+
}
|
|
497
|
+
lines = result.stdout
|
|
498
|
+
.split("\n")
|
|
499
|
+
.filter((ln) => ln.trim() !== "");
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const allPaths = [];
|
|
503
|
+
await this._walkDir(this.root, allPaths, _MAX_WALK_ENTRIES);
|
|
504
|
+
lines = allPaths.sort();
|
|
505
|
+
}
|
|
506
|
+
if (lines.length === 0) {
|
|
507
|
+
return "(no files)";
|
|
508
|
+
}
|
|
509
|
+
const clipped = lines.slice(0, this.maxFilesListed);
|
|
510
|
+
let suffix = "";
|
|
511
|
+
if (lines.length > clipped.length) {
|
|
512
|
+
suffix = `\n...[omitted ${lines.length - clipped.length} files]...`;
|
|
513
|
+
}
|
|
514
|
+
return clipped.join("\n") + suffix;
|
|
515
|
+
}
|
|
516
|
+
async searchFiles(query, glob) {
|
|
517
|
+
if (!query.trim()) {
|
|
518
|
+
return "query cannot be empty";
|
|
519
|
+
}
|
|
520
|
+
if (this._hasRg()) {
|
|
521
|
+
const cmd = ["rg", "-n", "--hidden", "-S", query, "."];
|
|
522
|
+
if (glob) {
|
|
523
|
+
cmd.push("-g", glob);
|
|
524
|
+
}
|
|
525
|
+
const result = await this._spawnCapture(cmd, this.root, this.commandTimeoutSec);
|
|
526
|
+
if (result === null) {
|
|
527
|
+
return "(search_files timed out)";
|
|
528
|
+
}
|
|
529
|
+
const outLines = result.stdout
|
|
530
|
+
.split("\n")
|
|
531
|
+
.filter((ln) => ln.trim() !== "");
|
|
532
|
+
if (outLines.length === 0) {
|
|
533
|
+
return "(no matches)";
|
|
534
|
+
}
|
|
535
|
+
const clipped = outLines.slice(0, this.maxSearchHits);
|
|
536
|
+
let suffix = "";
|
|
537
|
+
if (outLines.length > clipped.length) {
|
|
538
|
+
suffix = `\n...[omitted ${outLines.length - clipped.length} matches]...`;
|
|
539
|
+
}
|
|
540
|
+
return clipped.join("\n") + suffix;
|
|
541
|
+
}
|
|
542
|
+
// Fallback: manual walk + search
|
|
543
|
+
const matches = [];
|
|
544
|
+
const lowerQuery = query.toLowerCase();
|
|
545
|
+
const allPaths = [];
|
|
546
|
+
await this._walkDir(this.root, allPaths, _MAX_WALK_ENTRIES);
|
|
547
|
+
for (const rel of allPaths) {
|
|
548
|
+
const full = path.join(this.root, rel);
|
|
549
|
+
let text;
|
|
550
|
+
try {
|
|
551
|
+
text = await fsp.readFile(full, "utf-8");
|
|
552
|
+
}
|
|
553
|
+
catch {
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const fileLines = text.split("\n");
|
|
557
|
+
for (let idx = 0; idx < fileLines.length; idx++) {
|
|
558
|
+
if (fileLines[idx].toLowerCase().includes(lowerQuery)) {
|
|
559
|
+
matches.push(`${rel}:${idx + 1}:${fileLines[idx]}`);
|
|
560
|
+
if (matches.length >= this.maxSearchHits) {
|
|
561
|
+
return matches.join("\n") + "\n...[match limit reached]...";
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
return matches.length > 0 ? matches.join("\n") : "(no matches)";
|
|
567
|
+
}
|
|
568
|
+
// ------------------------------------------------------------------
|
|
569
|
+
// Repo map
|
|
570
|
+
// ------------------------------------------------------------------
|
|
571
|
+
async repoMap(glob, maxFiles = 200) {
|
|
572
|
+
const clamped = Math.max(1, Math.min(Math.floor(maxFiles), 500));
|
|
573
|
+
const candidates = await this._repoFiles(glob, clamped);
|
|
574
|
+
if (candidates.length === 0) {
|
|
575
|
+
return "(no files)";
|
|
576
|
+
}
|
|
577
|
+
const languageBySuffix = {
|
|
578
|
+
".py": "python",
|
|
579
|
+
".js": "javascript",
|
|
580
|
+
".jsx": "javascript",
|
|
581
|
+
".ts": "typescript",
|
|
582
|
+
".tsx": "typescript",
|
|
583
|
+
".go": "go",
|
|
584
|
+
".rs": "rust",
|
|
585
|
+
".java": "java",
|
|
586
|
+
".c": "c",
|
|
587
|
+
".h": "c",
|
|
588
|
+
".cpp": "cpp",
|
|
589
|
+
".hpp": "cpp",
|
|
590
|
+
".cs": "csharp",
|
|
591
|
+
".rb": "ruby",
|
|
592
|
+
".php": "php",
|
|
593
|
+
".swift": "swift",
|
|
594
|
+
".kt": "kotlin",
|
|
595
|
+
".scala": "scala",
|
|
596
|
+
".sh": "shell",
|
|
597
|
+
};
|
|
598
|
+
const files = [];
|
|
599
|
+
for (const rel of candidates) {
|
|
600
|
+
const suffix = path.extname(rel).toLowerCase();
|
|
601
|
+
const language = languageBySuffix[suffix];
|
|
602
|
+
if (!language)
|
|
603
|
+
continue;
|
|
604
|
+
let resolved;
|
|
605
|
+
try {
|
|
606
|
+
resolved = this._resolvePath(rel);
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
let stat;
|
|
612
|
+
try {
|
|
613
|
+
stat = await fsp.stat(resolved);
|
|
614
|
+
}
|
|
615
|
+
catch {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (!stat.isFile())
|
|
619
|
+
continue;
|
|
620
|
+
let text;
|
|
621
|
+
try {
|
|
622
|
+
text = await fsp.readFile(resolved, "utf-8");
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const symbols = this._genericSymbols(text);
|
|
628
|
+
files.push({
|
|
629
|
+
path: rel,
|
|
630
|
+
language,
|
|
631
|
+
lines: text.split("\n").length,
|
|
632
|
+
symbols: symbols.slice(0, 200),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
const output = {
|
|
636
|
+
root: this.root,
|
|
637
|
+
files,
|
|
638
|
+
total: files.length,
|
|
639
|
+
};
|
|
640
|
+
return this._clip(JSON.stringify(output, null, 2), this.maxFileChars);
|
|
641
|
+
}
|
|
642
|
+
// ------------------------------------------------------------------
|
|
643
|
+
// File read/write/edit
|
|
644
|
+
// ------------------------------------------------------------------
|
|
645
|
+
async readFile(filePath, hashline = true) {
|
|
646
|
+
const resolved = this._resolvePath(filePath);
|
|
647
|
+
let stat;
|
|
648
|
+
try {
|
|
649
|
+
stat = await fsp.stat(resolved);
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
return `File not found: ${filePath}`;
|
|
653
|
+
}
|
|
654
|
+
if (stat.isDirectory()) {
|
|
655
|
+
return `Path is a directory, not a file: ${filePath}`;
|
|
656
|
+
}
|
|
657
|
+
let text;
|
|
658
|
+
try {
|
|
659
|
+
text = await fsp.readFile(resolved, "utf-8");
|
|
660
|
+
}
|
|
661
|
+
catch (exc) {
|
|
662
|
+
return `Failed to read file ${filePath}: ${exc}`;
|
|
663
|
+
}
|
|
664
|
+
this._filesRead.add(resolved);
|
|
665
|
+
const clipped = this._clip(text, this.maxFileChars);
|
|
666
|
+
const rel = path.relative(this.root, resolved).split(path.sep).join("/");
|
|
667
|
+
const lines = clipped.split("\n");
|
|
668
|
+
let numbered;
|
|
669
|
+
if (hashline) {
|
|
670
|
+
numbered = lines
|
|
671
|
+
.map((line, i) => `${i + 1}:${_line_hash(line)}|${line}`)
|
|
672
|
+
.join("\n");
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
numbered = lines.map((line, i) => `${i + 1}|${line}`).join("\n");
|
|
676
|
+
}
|
|
677
|
+
return `# ${rel}\n${numbered}`;
|
|
678
|
+
}
|
|
679
|
+
async writeFile(filePath, content) {
|
|
680
|
+
const resolved = this._resolvePath(filePath);
|
|
681
|
+
let exists = false;
|
|
682
|
+
let isFile = false;
|
|
683
|
+
try {
|
|
684
|
+
const stat = await fsp.stat(resolved);
|
|
685
|
+
exists = true;
|
|
686
|
+
isFile = stat.isFile();
|
|
687
|
+
}
|
|
688
|
+
catch {
|
|
689
|
+
// does not exist
|
|
690
|
+
}
|
|
691
|
+
if (exists && isFile && !this._filesRead.has(resolved)) {
|
|
692
|
+
return (`BLOCKED: ${filePath} already exists but has not been read. ` +
|
|
693
|
+
`Use read_file('${filePath}') first, then edit via apply_patch or write_file.`);
|
|
694
|
+
}
|
|
695
|
+
try {
|
|
696
|
+
this._registerWriteTarget(resolved);
|
|
697
|
+
}
|
|
698
|
+
catch (exc) {
|
|
699
|
+
return `Blocked by policy: ${exc}`;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
await fsp.mkdir(path.dirname(resolved), { recursive: true });
|
|
703
|
+
await fsp.writeFile(resolved, content, "utf-8");
|
|
704
|
+
}
|
|
705
|
+
catch (exc) {
|
|
706
|
+
return `Failed to write ${filePath}: ${exc}`;
|
|
707
|
+
}
|
|
708
|
+
this._filesRead.add(resolved);
|
|
709
|
+
const rel = path.relative(this.root, resolved).split(path.sep).join("/");
|
|
710
|
+
return `Wrote ${content.length} chars to ${rel}`;
|
|
711
|
+
}
|
|
712
|
+
async editFile(filePath, oldText, newText) {
|
|
713
|
+
const resolved = this._resolvePath(filePath);
|
|
714
|
+
let stat;
|
|
715
|
+
try {
|
|
716
|
+
stat = await fsp.stat(resolved);
|
|
717
|
+
}
|
|
718
|
+
catch {
|
|
719
|
+
return `File not found: ${filePath}`;
|
|
720
|
+
}
|
|
721
|
+
if (stat.isDirectory()) {
|
|
722
|
+
return `Path is a directory, not a file: ${filePath}`;
|
|
723
|
+
}
|
|
724
|
+
let content;
|
|
725
|
+
try {
|
|
726
|
+
content = await fsp.readFile(resolved, "utf-8");
|
|
727
|
+
}
|
|
728
|
+
catch (exc) {
|
|
729
|
+
return `Failed to read file ${filePath}: ${exc}`;
|
|
730
|
+
}
|
|
731
|
+
this._filesRead.add(resolved);
|
|
732
|
+
if (!content.includes(oldText)) {
|
|
733
|
+
// Fuzzy fallback: whitespace-normalized match
|
|
734
|
+
const normOld = oldText.split(/\s+/).join(" ");
|
|
735
|
+
const oldLines = oldText.split(/\n/);
|
|
736
|
+
const contentLines = content.split(/\n/);
|
|
737
|
+
let found = false;
|
|
738
|
+
for (let i = 0; i <= contentLines.length - oldLines.length; i++) {
|
|
739
|
+
const candidate = contentLines.slice(i, i + oldLines.length).join("\n");
|
|
740
|
+
if (candidate.split(/\s+/).join(" ") === normOld) {
|
|
741
|
+
const before = contentLines.slice(0, i).join("\n");
|
|
742
|
+
const after = contentLines.slice(i + oldLines.length).join("\n");
|
|
743
|
+
content =
|
|
744
|
+
(before ? before + "\n" : "") +
|
|
745
|
+
newText +
|
|
746
|
+
(after ? "\n" + after : "");
|
|
747
|
+
found = true;
|
|
748
|
+
break;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (!found) {
|
|
752
|
+
return `edit_file failed: old_text not found in ${filePath}`;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
const count = content.split(oldText).length - 1;
|
|
757
|
+
if (count > 1) {
|
|
758
|
+
return `edit_file failed: old_text appears ${count} times in ${filePath}. Provide more context to make it unique.`;
|
|
759
|
+
}
|
|
760
|
+
content = content.replace(oldText, newText);
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
this._registerWriteTarget(resolved);
|
|
764
|
+
}
|
|
765
|
+
catch (exc) {
|
|
766
|
+
return `Blocked by policy: ${exc}`;
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
await fsp.writeFile(resolved, content, "utf-8");
|
|
770
|
+
}
|
|
771
|
+
catch (exc) {
|
|
772
|
+
return `Failed to write ${filePath}: ${exc}`;
|
|
773
|
+
}
|
|
774
|
+
this._filesRead.add(resolved);
|
|
775
|
+
const rel = path.relative(this.root, resolved).split(path.sep).join("/");
|
|
776
|
+
return `Edited ${rel}`;
|
|
777
|
+
}
|
|
778
|
+
// ------------------------------------------------------------------
|
|
779
|
+
// Hashline edit
|
|
780
|
+
// ------------------------------------------------------------------
|
|
781
|
+
_validateAnchor(anchor, lineHashes, lines) {
|
|
782
|
+
const parts = anchor.split(":");
|
|
783
|
+
if (parts.length !== 2 ||
|
|
784
|
+
!/^\d+$/.test(parts[0]) ||
|
|
785
|
+
parts[1].length !== 2) {
|
|
786
|
+
return [-1, `Invalid anchor format: '${anchor}' (expected N:HH)`];
|
|
787
|
+
}
|
|
788
|
+
const lineno = parseInt(parts[0], 10);
|
|
789
|
+
const expectedHash = parts[1];
|
|
790
|
+
if (lineno < 1 || lineno > lines.length) {
|
|
791
|
+
return [
|
|
792
|
+
-1,
|
|
793
|
+
`Line ${lineno} out of range (file has ${lines.length} lines)`,
|
|
794
|
+
];
|
|
795
|
+
}
|
|
796
|
+
const actualHash = lineHashes.get(lineno);
|
|
797
|
+
if (actualHash !== expectedHash) {
|
|
798
|
+
const ctxStart = Math.max(1, lineno - 2);
|
|
799
|
+
const ctxEnd = Math.min(lines.length, lineno + 2);
|
|
800
|
+
const ctxLines = [];
|
|
801
|
+
for (let i = ctxStart; i <= ctxEnd; i++) {
|
|
802
|
+
ctxLines.push(` ${i}:${lineHashes.get(i)}|${lines[i - 1]}`);
|
|
803
|
+
}
|
|
804
|
+
return [
|
|
805
|
+
-1,
|
|
806
|
+
`Hash mismatch at line ${lineno}: expected ${expectedHash}, ` +
|
|
807
|
+
`got ${actualHash}. Current context:\n` +
|
|
808
|
+
ctxLines.join("\n"),
|
|
809
|
+
];
|
|
810
|
+
}
|
|
811
|
+
return [lineno, null];
|
|
812
|
+
}
|
|
813
|
+
async hashlineEdit(filePath, edits) {
|
|
814
|
+
const resolved = this._resolvePath(filePath);
|
|
815
|
+
let stat;
|
|
816
|
+
try {
|
|
817
|
+
stat = await fsp.stat(resolved);
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
return `File not found: ${filePath}`;
|
|
821
|
+
}
|
|
822
|
+
if (stat.isDirectory()) {
|
|
823
|
+
return `Path is a directory, not a file: ${filePath}`;
|
|
824
|
+
}
|
|
825
|
+
let content;
|
|
826
|
+
try {
|
|
827
|
+
content = await fsp.readFile(resolved, "utf-8");
|
|
828
|
+
}
|
|
829
|
+
catch (exc) {
|
|
830
|
+
return `Failed to read file ${filePath}: ${exc}`;
|
|
831
|
+
}
|
|
832
|
+
this._filesRead.add(resolved);
|
|
833
|
+
const lines = content.split("\n");
|
|
834
|
+
const lineHashes = new Map();
|
|
835
|
+
for (let i = 0; i < lines.length; i++) {
|
|
836
|
+
lineHashes.set(i + 1, _line_hash(lines[i]));
|
|
837
|
+
}
|
|
838
|
+
const parsed = [];
|
|
839
|
+
for (const edit of edits) {
|
|
840
|
+
if ("set_line" in edit) {
|
|
841
|
+
const anchor = String(edit.set_line);
|
|
842
|
+
const [lineno, err] = this._validateAnchor(anchor, lineHashes, lines);
|
|
843
|
+
if (err)
|
|
844
|
+
return err;
|
|
845
|
+
const raw = String(edit.content ?? "");
|
|
846
|
+
const newLine = raw.replace(_HASHLINE_PREFIX_RE, "");
|
|
847
|
+
parsed.push({ op: "set", start: lineno, end: lineno, newLines: [newLine] });
|
|
848
|
+
}
|
|
849
|
+
else if ("replace_lines" in edit) {
|
|
850
|
+
const rng = edit.replace_lines;
|
|
851
|
+
const startAnchor = String(rng.start ?? "");
|
|
852
|
+
const endAnchor = String(rng.end ?? "");
|
|
853
|
+
const [start, errStart] = this._validateAnchor(startAnchor, lineHashes, lines);
|
|
854
|
+
if (errStart)
|
|
855
|
+
return errStart;
|
|
856
|
+
const [end, errEnd] = this._validateAnchor(endAnchor, lineHashes, lines);
|
|
857
|
+
if (errEnd)
|
|
858
|
+
return errEnd;
|
|
859
|
+
if (end < start) {
|
|
860
|
+
return `End line ${end} is before start line ${start}`;
|
|
861
|
+
}
|
|
862
|
+
const rawContent = String(edit.content ?? "");
|
|
863
|
+
const newLines = rawContent
|
|
864
|
+
.split("\n")
|
|
865
|
+
.map((ln) => ln.replace(_HASHLINE_PREFIX_RE, ""));
|
|
866
|
+
parsed.push({ op: "replace", start, end, newLines });
|
|
867
|
+
}
|
|
868
|
+
else if ("insert_after" in edit) {
|
|
869
|
+
const anchor = String(edit.insert_after);
|
|
870
|
+
const [lineno, err] = this._validateAnchor(anchor, lineHashes, lines);
|
|
871
|
+
if (err)
|
|
872
|
+
return err;
|
|
873
|
+
const rawContent = String(edit.content ?? "");
|
|
874
|
+
const newLines = rawContent
|
|
875
|
+
.split("\n")
|
|
876
|
+
.map((ln) => ln.replace(_HASHLINE_PREFIX_RE, ""));
|
|
877
|
+
parsed.push({ op: "insert", start: lineno, end: lineno, newLines });
|
|
878
|
+
}
|
|
879
|
+
else {
|
|
880
|
+
return `Unknown edit operation: ${JSON.stringify(edit)}. Use set_line, replace_lines, or insert_after.`;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// Sort by line number descending so bottom-up application doesn't shift indices
|
|
884
|
+
parsed.sort((a, b) => b.start - a.start);
|
|
885
|
+
// Apply edits
|
|
886
|
+
let changed = 0;
|
|
887
|
+
for (const { op, start, end, newLines } of parsed) {
|
|
888
|
+
if (op === "set") {
|
|
889
|
+
if (lines[start - 1] !== newLines[0]) {
|
|
890
|
+
lines[start - 1] = newLines[0];
|
|
891
|
+
changed += 1;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
else if (op === "replace") {
|
|
895
|
+
const oldSlice = lines.slice(start - 1, end);
|
|
896
|
+
if (oldSlice.length !== newLines.length ||
|
|
897
|
+
oldSlice.some((l, i) => l !== newLines[i])) {
|
|
898
|
+
lines.splice(start - 1, end - start + 1, ...newLines);
|
|
899
|
+
changed += 1;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
else if (op === "insert") {
|
|
903
|
+
lines.splice(start, 0, ...newLines);
|
|
904
|
+
changed += 1;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (changed === 0) {
|
|
908
|
+
return `No changes needed in ${filePath}`;
|
|
909
|
+
}
|
|
910
|
+
let newContent = lines.join("\n");
|
|
911
|
+
if (content.endsWith("\n")) {
|
|
912
|
+
newContent += "\n";
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
this._registerWriteTarget(resolved);
|
|
916
|
+
}
|
|
917
|
+
catch (exc) {
|
|
918
|
+
return `Blocked by policy: ${exc}`;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
await fsp.writeFile(resolved, newContent, "utf-8");
|
|
922
|
+
}
|
|
923
|
+
catch (exc) {
|
|
924
|
+
return `Failed to write ${filePath}: ${exc}`;
|
|
925
|
+
}
|
|
926
|
+
this._filesRead.add(resolved);
|
|
927
|
+
const rel = path.relative(this.root, resolved).split(path.sep).join("/");
|
|
928
|
+
return `Edited ${rel} (${changed} edit(s) applied)`;
|
|
929
|
+
}
|
|
930
|
+
// ------------------------------------------------------------------
|
|
931
|
+
// Patch
|
|
932
|
+
// ------------------------------------------------------------------
|
|
933
|
+
async applyPatch(patchText) {
|
|
934
|
+
if (!patchText.trim()) {
|
|
935
|
+
return "apply_patch requires non-empty patch text";
|
|
936
|
+
}
|
|
937
|
+
let ops;
|
|
938
|
+
try {
|
|
939
|
+
ops = parse_agent_patch(patchText);
|
|
940
|
+
}
|
|
941
|
+
catch (exc) {
|
|
942
|
+
if (exc instanceof PatchApplyError) {
|
|
943
|
+
return `Patch failed: ${exc.message}`;
|
|
944
|
+
}
|
|
945
|
+
throw exc;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
for (const op of ops) {
|
|
949
|
+
if ("plus_lines" in op) {
|
|
950
|
+
// AddFileOp
|
|
951
|
+
this._registerWriteTarget(this._resolvePath(op.path));
|
|
952
|
+
}
|
|
953
|
+
else if ("raw_lines" in op) {
|
|
954
|
+
// UpdateFileOp
|
|
955
|
+
this._registerWriteTarget(this._resolvePath(op.path));
|
|
956
|
+
if (op.move_to) {
|
|
957
|
+
this._registerWriteTarget(this._resolvePath(op.move_to));
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
// DeleteFileOp
|
|
962
|
+
this._registerWriteTarget(this._resolvePath(op.path));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
catch (exc) {
|
|
967
|
+
if (exc instanceof ToolError) {
|
|
968
|
+
return `Blocked by policy: ${exc.message}`;
|
|
969
|
+
}
|
|
970
|
+
return `Blocked by policy: ${String(exc)}`;
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
const report = apply_agent_patch(patchText, (rawPath) => this._resolvePath(rawPath));
|
|
974
|
+
for (const relPath of [...report.added, ...report.updated]) {
|
|
975
|
+
try {
|
|
976
|
+
this._filesRead.add(this._resolvePath(relPath));
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
// ignore
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return report.render();
|
|
983
|
+
}
|
|
984
|
+
catch (exc) {
|
|
985
|
+
if (exc instanceof PatchApplyError) {
|
|
986
|
+
return `Patch failed: ${exc.message}`;
|
|
987
|
+
}
|
|
988
|
+
return `Patch failed: ${String(exc)}`;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
// ------------------------------------------------------------------
|
|
992
|
+
// Exa API: web_search and fetch_url
|
|
993
|
+
// ------------------------------------------------------------------
|
|
994
|
+
static _EXA_MAX_RETRIES = 3;
|
|
995
|
+
static _EXA_BACKOFF_BASE = 1.0;
|
|
996
|
+
_exaDiagnoseNetwork() {
|
|
997
|
+
// Synchronous quick-check diagnostics
|
|
998
|
+
const hints = [];
|
|
999
|
+
for (const envVar of [
|
|
1000
|
+
"HTTPS_PROXY",
|
|
1001
|
+
"https_proxy",
|
|
1002
|
+
"HTTP_PROXY",
|
|
1003
|
+
"http_proxy",
|
|
1004
|
+
]) {
|
|
1005
|
+
const val = process.env[envVar];
|
|
1006
|
+
if (val) {
|
|
1007
|
+
hints.push(`proxy ${envVar}=${val} is set`);
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return hints.length > 0
|
|
1012
|
+
? hints.join("; ")
|
|
1013
|
+
: "no obvious network issue detected";
|
|
1014
|
+
}
|
|
1015
|
+
async _exaRequest(endpoint, payload) {
|
|
1016
|
+
if (!this.exaApiKey?.trim()) {
|
|
1017
|
+
throw new ToolError("EXA_API_KEY not configured");
|
|
1018
|
+
}
|
|
1019
|
+
const url = new URL(endpoint, this.exaBaseUrl.replace(/\/+$/, "") + "/");
|
|
1020
|
+
const data = JSON.stringify(payload);
|
|
1021
|
+
const headers = {
|
|
1022
|
+
"x-api-key": this.exaApiKey,
|
|
1023
|
+
"Content-Type": "application/json",
|
|
1024
|
+
"User-Agent": "exa-py 1.0.18",
|
|
1025
|
+
};
|
|
1026
|
+
let lastExc = null;
|
|
1027
|
+
for (let attempt = 0; attempt < WorkspaceTools._EXA_MAX_RETRIES; attempt++) {
|
|
1028
|
+
try {
|
|
1029
|
+
const raw = await this._httpPost(url.href, data, headers);
|
|
1030
|
+
const parsed = JSON.parse(raw);
|
|
1031
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1032
|
+
throw new ToolError(`Exa API returned non-object response: ${typeof parsed}`);
|
|
1033
|
+
}
|
|
1034
|
+
return parsed;
|
|
1035
|
+
}
|
|
1036
|
+
catch (exc) {
|
|
1037
|
+
if (exc instanceof ToolError) {
|
|
1038
|
+
throw exc;
|
|
1039
|
+
}
|
|
1040
|
+
if (exc instanceof HttpError &&
|
|
1041
|
+
exc.statusCode < 500 &&
|
|
1042
|
+
exc.statusCode !== 429) {
|
|
1043
|
+
throw new ToolError(`Exa API HTTP ${exc.statusCode}: ${exc.body}`);
|
|
1044
|
+
}
|
|
1045
|
+
lastExc = exc;
|
|
1046
|
+
if (attempt < WorkspaceTools._EXA_MAX_RETRIES - 1) {
|
|
1047
|
+
await this._sleep(WorkspaceTools._EXA_BACKOFF_BASE * 2 ** attempt);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
const diag = this._exaDiagnoseNetwork();
|
|
1052
|
+
throw new ToolError(`Exa API error after ${WorkspaceTools._EXA_MAX_RETRIES} attempts: ${lastExc} (diagnostics: ${diag})`);
|
|
1053
|
+
}
|
|
1054
|
+
async webSearch(query, numResults = 10, includeText = false) {
|
|
1055
|
+
const trimmed = query.trim();
|
|
1056
|
+
if (!trimmed) {
|
|
1057
|
+
return "web_search requires non-empty query";
|
|
1058
|
+
}
|
|
1059
|
+
const clampedResults = Math.max(1, Math.min(Math.floor(numResults), 20));
|
|
1060
|
+
const payload = {
|
|
1061
|
+
query: trimmed,
|
|
1062
|
+
numResults: clampedResults,
|
|
1063
|
+
};
|
|
1064
|
+
if (includeText) {
|
|
1065
|
+
payload.contents = { text: { maxCharacters: 4000 } };
|
|
1066
|
+
}
|
|
1067
|
+
let parsed;
|
|
1068
|
+
try {
|
|
1069
|
+
parsed = await this._exaRequest("/search", payload);
|
|
1070
|
+
}
|
|
1071
|
+
catch (exc) {
|
|
1072
|
+
return `Web search failed: ${exc}`;
|
|
1073
|
+
}
|
|
1074
|
+
const outResults = [];
|
|
1075
|
+
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
1076
|
+
for (const row of results) {
|
|
1077
|
+
if (typeof row !== "object" || row === null)
|
|
1078
|
+
continue;
|
|
1079
|
+
const r = row;
|
|
1080
|
+
const item = {
|
|
1081
|
+
url: String(r.url ?? ""),
|
|
1082
|
+
title: String(r.title ?? ""),
|
|
1083
|
+
snippet: String(r.highlight ?? r.snippet ?? ""),
|
|
1084
|
+
};
|
|
1085
|
+
if (includeText && typeof r.text === "string") {
|
|
1086
|
+
item.text = this._clip(r.text, 4000);
|
|
1087
|
+
}
|
|
1088
|
+
outResults.push(item);
|
|
1089
|
+
}
|
|
1090
|
+
const output = {
|
|
1091
|
+
query: trimmed,
|
|
1092
|
+
results: outResults,
|
|
1093
|
+
total: outResults.length,
|
|
1094
|
+
};
|
|
1095
|
+
return this._clip(JSON.stringify(output, null, 2), this.maxFileChars);
|
|
1096
|
+
}
|
|
1097
|
+
async fetchUrl(urls) {
|
|
1098
|
+
if (!Array.isArray(urls)) {
|
|
1099
|
+
return "fetch_url requires a list of URL strings";
|
|
1100
|
+
}
|
|
1101
|
+
const normalized = [];
|
|
1102
|
+
for (const raw of urls) {
|
|
1103
|
+
if (typeof raw !== "string")
|
|
1104
|
+
continue;
|
|
1105
|
+
const text = raw.trim();
|
|
1106
|
+
if (text)
|
|
1107
|
+
normalized.push(text);
|
|
1108
|
+
}
|
|
1109
|
+
if (normalized.length === 0) {
|
|
1110
|
+
return "fetch_url requires at least one valid URL";
|
|
1111
|
+
}
|
|
1112
|
+
const capped = normalized.slice(0, 10);
|
|
1113
|
+
const payload = {
|
|
1114
|
+
ids: capped,
|
|
1115
|
+
text: { maxCharacters: 8000 },
|
|
1116
|
+
};
|
|
1117
|
+
let parsed;
|
|
1118
|
+
try {
|
|
1119
|
+
parsed = await this._exaRequest("/contents", payload);
|
|
1120
|
+
}
|
|
1121
|
+
catch (exc) {
|
|
1122
|
+
return `Fetch URL failed: ${exc}`;
|
|
1123
|
+
}
|
|
1124
|
+
const pages = [];
|
|
1125
|
+
const results = Array.isArray(parsed.results) ? parsed.results : [];
|
|
1126
|
+
for (const row of results) {
|
|
1127
|
+
if (typeof row !== "object" || row === null)
|
|
1128
|
+
continue;
|
|
1129
|
+
const r = row;
|
|
1130
|
+
pages.push({
|
|
1131
|
+
url: String(r.url ?? ""),
|
|
1132
|
+
title: String(r.title ?? ""),
|
|
1133
|
+
text: this._clip(String(r.text ?? ""), 8000),
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
const output = {
|
|
1137
|
+
pages,
|
|
1138
|
+
total: pages.length,
|
|
1139
|
+
};
|
|
1140
|
+
return this._clip(JSON.stringify(output, null, 2), this.maxFileChars);
|
|
1141
|
+
}
|
|
1142
|
+
// ------------------------------------------------------------------
|
|
1143
|
+
// Domain tools
|
|
1144
|
+
// ------------------------------------------------------------------
|
|
1145
|
+
async runDomainTool(toolName, args) {
|
|
1146
|
+
const script = WorkspaceTools._DOMAIN_TOOL_SCRIPTS[toolName];
|
|
1147
|
+
if (!script) {
|
|
1148
|
+
return `Unknown domain tool: ${toolName}`;
|
|
1149
|
+
}
|
|
1150
|
+
// Navigate up from this file's location to the project root, then into tools/
|
|
1151
|
+
const packageDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
|
|
1152
|
+
const scriptPath = path.join(packageDir, "tools", script);
|
|
1153
|
+
if (!fs.existsSync(scriptPath)) {
|
|
1154
|
+
return `Domain tool script not found: ${scriptPath}`;
|
|
1155
|
+
}
|
|
1156
|
+
const argMap = WorkspaceTools._DOMAIN_ARG_MAP[toolName] ?? {};
|
|
1157
|
+
const cmdParts = ["python3", scriptPath];
|
|
1158
|
+
for (const [paramName, cliFlag] of Object.entries(argMap)) {
|
|
1159
|
+
const value = args[paramName];
|
|
1160
|
+
if (value === undefined || value === null)
|
|
1161
|
+
continue;
|
|
1162
|
+
if (typeof value === "boolean") {
|
|
1163
|
+
if (value)
|
|
1164
|
+
cmdParts.push(cliFlag);
|
|
1165
|
+
}
|
|
1166
|
+
else if (Array.isArray(value)) {
|
|
1167
|
+
cmdParts.push(cliFlag);
|
|
1168
|
+
for (const item of value) {
|
|
1169
|
+
cmdParts.push(String(item));
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
else {
|
|
1173
|
+
cmdParts.push(cliFlag, String(value));
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const timeout = Math.max(this.commandTimeoutSec, 120);
|
|
1177
|
+
return new Promise((resolve) => {
|
|
1178
|
+
let proc;
|
|
1179
|
+
try {
|
|
1180
|
+
proc = spawn(cmdParts[0], cmdParts.slice(1), {
|
|
1181
|
+
cwd: this.root,
|
|
1182
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1183
|
+
detached: true,
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
catch (exc) {
|
|
1187
|
+
resolve(`[${toolName}] failed to start: ${exc}`);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
let stdout = "";
|
|
1191
|
+
let stderr = "";
|
|
1192
|
+
let finished = false;
|
|
1193
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1194
|
+
stdout += chunk.toString("utf-8");
|
|
1195
|
+
});
|
|
1196
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1197
|
+
stderr += chunk.toString("utf-8");
|
|
1198
|
+
});
|
|
1199
|
+
const timer = setTimeout(() => {
|
|
1200
|
+
if (!finished) {
|
|
1201
|
+
finished = true;
|
|
1202
|
+
try {
|
|
1203
|
+
if (proc.pid !== undefined) {
|
|
1204
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
catch {
|
|
1208
|
+
try {
|
|
1209
|
+
proc.kill("SIGKILL");
|
|
1210
|
+
}
|
|
1211
|
+
catch {
|
|
1212
|
+
// ignore
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
resolve(`[${toolName}] timeout after ${timeout}s`);
|
|
1216
|
+
}
|
|
1217
|
+
}, timeout * 1000);
|
|
1218
|
+
proc.on("close", (code) => {
|
|
1219
|
+
if (!finished) {
|
|
1220
|
+
finished = true;
|
|
1221
|
+
clearTimeout(timer);
|
|
1222
|
+
let merged = "";
|
|
1223
|
+
if (stdout.trim())
|
|
1224
|
+
merged += stdout;
|
|
1225
|
+
if (stderr.trim())
|
|
1226
|
+
merged += `\n[stderr]\n${stderr}`;
|
|
1227
|
+
if (code !== 0) {
|
|
1228
|
+
merged = `[${toolName} exited with code ${code}]\n${merged}`;
|
|
1229
|
+
}
|
|
1230
|
+
resolve(this._clip(merged.trim(), this.maxShellOutputChars));
|
|
1231
|
+
}
|
|
1232
|
+
});
|
|
1233
|
+
proc.on("error", (err) => {
|
|
1234
|
+
if (!finished) {
|
|
1235
|
+
finished = true;
|
|
1236
|
+
clearTimeout(timer);
|
|
1237
|
+
resolve(`[${toolName}] failed to start: ${err}`);
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
// ------------------------------------------------------------------
|
|
1243
|
+
// Internal utilities
|
|
1244
|
+
// ------------------------------------------------------------------
|
|
1245
|
+
_hasRg() {
|
|
1246
|
+
try {
|
|
1247
|
+
const result = execSync("which rg", {
|
|
1248
|
+
stdio: "pipe",
|
|
1249
|
+
timeout: 5000,
|
|
1250
|
+
});
|
|
1251
|
+
return result.toString().trim().length > 0;
|
|
1252
|
+
}
|
|
1253
|
+
catch {
|
|
1254
|
+
return false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
_spawnCapture(cmd, cwd, timeoutSec) {
|
|
1258
|
+
return new Promise((resolve) => {
|
|
1259
|
+
let proc;
|
|
1260
|
+
try {
|
|
1261
|
+
proc = spawn(cmd[0], cmd.slice(1), {
|
|
1262
|
+
cwd,
|
|
1263
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1264
|
+
detached: true,
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
catch {
|
|
1268
|
+
resolve({ stdout: "", stderr: "", code: -1 });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
let stdout = "";
|
|
1272
|
+
let stderr = "";
|
|
1273
|
+
let finished = false;
|
|
1274
|
+
proc.stdout?.on("data", (chunk) => {
|
|
1275
|
+
stdout += chunk.toString("utf-8");
|
|
1276
|
+
});
|
|
1277
|
+
proc.stderr?.on("data", (chunk) => {
|
|
1278
|
+
stderr += chunk.toString("utf-8");
|
|
1279
|
+
});
|
|
1280
|
+
const timer = setTimeout(() => {
|
|
1281
|
+
if (!finished) {
|
|
1282
|
+
finished = true;
|
|
1283
|
+
try {
|
|
1284
|
+
if (proc.pid !== undefined) {
|
|
1285
|
+
process.kill(-proc.pid, "SIGKILL");
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
catch {
|
|
1289
|
+
try {
|
|
1290
|
+
proc.kill("SIGKILL");
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
// ignore
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
resolve(null);
|
|
1297
|
+
}
|
|
1298
|
+
}, timeoutSec * 1000);
|
|
1299
|
+
proc.on("close", (code) => {
|
|
1300
|
+
if (!finished) {
|
|
1301
|
+
finished = true;
|
|
1302
|
+
clearTimeout(timer);
|
|
1303
|
+
resolve({ stdout, stderr, code: code ?? -1 });
|
|
1304
|
+
}
|
|
1305
|
+
});
|
|
1306
|
+
proc.on("error", () => {
|
|
1307
|
+
if (!finished) {
|
|
1308
|
+
finished = true;
|
|
1309
|
+
clearTimeout(timer);
|
|
1310
|
+
resolve({ stdout: "", stderr: "", code: -1 });
|
|
1311
|
+
}
|
|
1312
|
+
});
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
async _walkDir(dir, allPaths, maxEntries) {
|
|
1316
|
+
if (allPaths.length >= maxEntries)
|
|
1317
|
+
return;
|
|
1318
|
+
let entries;
|
|
1319
|
+
try {
|
|
1320
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
1321
|
+
}
|
|
1322
|
+
catch {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
for (const entry of entries) {
|
|
1326
|
+
if (allPaths.length >= maxEntries)
|
|
1327
|
+
return;
|
|
1328
|
+
if (entry.name === ".git")
|
|
1329
|
+
continue;
|
|
1330
|
+
const full = path.join(dir, entry.name);
|
|
1331
|
+
if (entry.isDirectory()) {
|
|
1332
|
+
await this._walkDir(full, allPaths, maxEntries);
|
|
1333
|
+
}
|
|
1334
|
+
else if (entry.isFile()) {
|
|
1335
|
+
const rel = path.relative(this.root, full).split(path.sep).join("/");
|
|
1336
|
+
allPaths.push(rel);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
async _repoFiles(glob, maxFiles) {
|
|
1341
|
+
let lines;
|
|
1342
|
+
if (this._hasRg()) {
|
|
1343
|
+
const cmd = ["rg", "--files", "--hidden", "-g", "!.git"];
|
|
1344
|
+
if (glob) {
|
|
1345
|
+
cmd.push("-g", glob);
|
|
1346
|
+
}
|
|
1347
|
+
const result = await this._spawnCapture(cmd, this.root, this.commandTimeoutSec);
|
|
1348
|
+
if (!result)
|
|
1349
|
+
return [];
|
|
1350
|
+
lines = result.stdout.split("\n").filter((ln) => ln.trim() !== "");
|
|
1351
|
+
}
|
|
1352
|
+
else {
|
|
1353
|
+
const allPaths = [];
|
|
1354
|
+
await this._walkDir(this.root, allPaths, _MAX_WALK_ENTRIES);
|
|
1355
|
+
lines = allPaths;
|
|
1356
|
+
if (glob) {
|
|
1357
|
+
const globRe = this._globToRegex(glob);
|
|
1358
|
+
lines = lines.filter((l) => globRe.test(l));
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
return lines.slice(0, maxFiles);
|
|
1362
|
+
}
|
|
1363
|
+
_globToRegex(glob) {
|
|
1364
|
+
// Simple glob-to-regex: * -> [^/]*, ** -> .*, ? -> .
|
|
1365
|
+
let pattern = glob
|
|
1366
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
1367
|
+
.replace(/\*\*/g, "\0")
|
|
1368
|
+
.replace(/\*/g, "[^/]*")
|
|
1369
|
+
.replace(/\0/g, ".*")
|
|
1370
|
+
.replace(/\?/g, ".");
|
|
1371
|
+
return new RegExp(`^${pattern}$`);
|
|
1372
|
+
}
|
|
1373
|
+
_genericSymbols(text) {
|
|
1374
|
+
const patterns = [
|
|
1375
|
+
[
|
|
1376
|
+
/^\s*function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/gm,
|
|
1377
|
+
"function",
|
|
1378
|
+
],
|
|
1379
|
+
[/^\s*class\s+([A-Za-z_][A-Za-z0-9_]*)\b/gm, "class"],
|
|
1380
|
+
[
|
|
1381
|
+
/^\s*(?:const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\(/gm,
|
|
1382
|
+
"function",
|
|
1383
|
+
],
|
|
1384
|
+
];
|
|
1385
|
+
const symbols = [];
|
|
1386
|
+
for (const [regex, kind] of patterns) {
|
|
1387
|
+
regex.lastIndex = 0;
|
|
1388
|
+
let match;
|
|
1389
|
+
while ((match = regex.exec(text)) !== null) {
|
|
1390
|
+
const line = text.slice(0, match.index).split("\n").length;
|
|
1391
|
+
symbols.push({ kind, name: match[1], line });
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
symbols.sort((a, b) => a.line - b.line);
|
|
1395
|
+
return symbols;
|
|
1396
|
+
}
|
|
1397
|
+
_sleep(seconds) {
|
|
1398
|
+
return new Promise((resolve) => setTimeout(resolve, seconds * 1000));
|
|
1399
|
+
}
|
|
1400
|
+
_httpPost(url, body, headers) {
|
|
1401
|
+
return new Promise((resolve, reject) => {
|
|
1402
|
+
const parsed = new URL(url);
|
|
1403
|
+
const isHttps = parsed.protocol === "https:";
|
|
1404
|
+
const lib = isHttps ? https : http;
|
|
1405
|
+
const options = {
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
hostname: parsed.hostname,
|
|
1408
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
1409
|
+
path: parsed.pathname + parsed.search,
|
|
1410
|
+
headers: {
|
|
1411
|
+
...headers,
|
|
1412
|
+
"Content-Length": Buffer.byteLength(body, "utf-8").toString(),
|
|
1413
|
+
},
|
|
1414
|
+
timeout: this.commandTimeoutSec * 1000,
|
|
1415
|
+
};
|
|
1416
|
+
const req = lib.request(options, (res) => {
|
|
1417
|
+
let data = "";
|
|
1418
|
+
res.setEncoding("utf-8");
|
|
1419
|
+
res.on("data", (chunk) => {
|
|
1420
|
+
data += chunk;
|
|
1421
|
+
});
|
|
1422
|
+
res.on("end", () => {
|
|
1423
|
+
const statusCode = res.statusCode ?? 0;
|
|
1424
|
+
if (statusCode >= 200 && statusCode < 300) {
|
|
1425
|
+
resolve(data);
|
|
1426
|
+
}
|
|
1427
|
+
else {
|
|
1428
|
+
reject(new HttpError(statusCode, data));
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
});
|
|
1432
|
+
req.on("error", reject);
|
|
1433
|
+
req.on("timeout", () => {
|
|
1434
|
+
req.destroy(new Error("Request timeout"));
|
|
1435
|
+
});
|
|
1436
|
+
req.write(body);
|
|
1437
|
+
req.end();
|
|
1438
|
+
});
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
// ---------------------------------------------------------------------------
|
|
1442
|
+
// Internal HTTP error for retry logic
|
|
1443
|
+
// ---------------------------------------------------------------------------
|
|
1444
|
+
class HttpError extends Error {
|
|
1445
|
+
statusCode;
|
|
1446
|
+
body;
|
|
1447
|
+
constructor(statusCode, body) {
|
|
1448
|
+
super(`HTTP ${statusCode}`);
|
|
1449
|
+
this.name = "HttpError";
|
|
1450
|
+
this.statusCode = statusCode;
|
|
1451
|
+
this.body = body;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
//# sourceMappingURL=tools.js.map
|