@agentprojectcontext/apx 1.10.4 → 1.12.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.
@@ -0,0 +1,165 @@
1
+ // daemon/tools/glob.js
2
+ // Glob tool for APX — lists files matching a glob pattern.
3
+ // Backends, in order of preference:
4
+ // 1. fast-glob (npm) — full glob spec, brace expansion, negation patterns
5
+ // 2. node:fs/promises glob (Node 22+) — built-in, no extra dep
6
+ // 3. Manual walk + regex — pure-JS fallback for older Node
7
+ //
8
+ // Endpoint: POST /tools/glob body: { pattern, cwd?, dot?, absolute?, ignore?, limit? }
9
+ // GET /tools/glob?pattern=**/*.js&cwd=/path
10
+
11
+ import fs from "node:fs";
12
+ import path from "node:path";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // Manual fallback (pure JS — no glob lib, no Node 22+)
16
+ // ---------------------------------------------------------------------------
17
+
18
+ function globToRegex(pattern) {
19
+ let re = pattern
20
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
21
+ .replace(/\*\*\//g, "(?:.+/)?")
22
+ .replace(/\*\*/g, ".*")
23
+ .replace(/\*/g, "[^/]*")
24
+ .replace(/\?/g, "[^/]");
25
+ return new RegExp(`^${re}$`);
26
+ }
27
+
28
+ function walkDir(dir, { dot = false, maxFiles = 5000, ignoreFns = [] } = {}) {
29
+ const results = [];
30
+ const queue = [dir];
31
+ while (queue.length && results.length < maxFiles) {
32
+ const current = queue.shift();
33
+ let entries;
34
+ try {
35
+ entries = fs.readdirSync(current, { withFileTypes: true });
36
+ } catch { continue; }
37
+
38
+ for (const entry of entries) {
39
+ if (!dot && entry.name.startsWith(".")) continue;
40
+ const full = path.join(current, entry.name);
41
+ const rel = path.relative(dir, full);
42
+ if (ignoreFns.some(fn => fn(rel, full, entry.name))) continue;
43
+ if (entry.isDirectory()) {
44
+ queue.push(full);
45
+ } else if (entry.isFile() || entry.isSymbolicLink()) {
46
+ results.push(full);
47
+ }
48
+ }
49
+ }
50
+ return results;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Backend selection
55
+ // ---------------------------------------------------------------------------
56
+
57
+ async function loadFastGlob() {
58
+ try {
59
+ const mod = await import("fast-glob");
60
+ return mod.default ?? mod;
61
+ } catch {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ function normalizeIgnore(ignore) {
67
+ if (!ignore) return [];
68
+ return Array.isArray(ignore) ? ignore.filter(Boolean).map(String) : [String(ignore)];
69
+ }
70
+
71
+ export async function globFiles({ pattern, cwd, dot = false, absolute = false, ignore, limit = 500 } = {}) {
72
+ if (!pattern) throw new Error("pattern required");
73
+ const base = path.resolve(cwd || process.cwd());
74
+ if (!fs.existsSync(base)) throw new Error(`cwd does not exist: ${base}`);
75
+
76
+ const ignoreList = normalizeIgnore(ignore);
77
+ const lim = Math.min(parseInt(limit, 10) || 500, 5000);
78
+
79
+ let files = [];
80
+ let backend = "manual";
81
+
82
+ // Backend 1: fast-glob
83
+ const fg = await loadFastGlob();
84
+ if (fg) {
85
+ backend = "fast-glob";
86
+ files = await fg(pattern, {
87
+ cwd: base,
88
+ dot,
89
+ absolute: true,
90
+ ignore: ignoreList,
91
+ followSymbolicLinks: false,
92
+ suppressErrors: true,
93
+ });
94
+ files = files.slice(0, lim + 1);
95
+ } else {
96
+ // Backend 2: Node 22+ native
97
+ try {
98
+ const { glob: nodeGlob } = await import("node:fs/promises");
99
+ backend = "node-native";
100
+ const ignoreRegexes = ignoreList.map(globToRegex);
101
+ const matches = [];
102
+ for await (const f of nodeGlob(pattern, { cwd: base, dot, absolute: true })) {
103
+ const rel = path.relative(base, f);
104
+ if (ignoreRegexes.some(re => re.test(rel) || re.test(path.basename(f)))) continue;
105
+ matches.push(f);
106
+ if (matches.length > lim) break;
107
+ }
108
+ files = matches;
109
+ } catch {
110
+ // Backend 3: pure-JS walk + regex
111
+ backend = "manual";
112
+ const re = globToRegex(pattern);
113
+ const ignoreRegexes = ignoreList.map(globToRegex);
114
+ const ignoreFns = [
115
+ (rel, _full, name) => ignoreRegexes.some(r => r.test(rel) || r.test(name)),
116
+ ];
117
+ const all = walkDir(base, { dot, maxFiles: lim * 4, ignoreFns });
118
+ files = all
119
+ .filter(f => re.test(path.relative(base, f)))
120
+ .slice(0, lim + 1);
121
+ }
122
+ }
123
+
124
+ const truncated = files.length > lim;
125
+ files = files.slice(0, lim);
126
+
127
+ const result = files.map((f) => {
128
+ const rel = path.relative(base, f);
129
+ const stat = (() => { try { return fs.statSync(f); } catch { return null; } })();
130
+ return absolute
131
+ ? { path: f, relative: rel, size: stat?.size ?? null }
132
+ : { path: rel, absolute: f, size: stat?.size ?? null };
133
+ });
134
+
135
+ return {
136
+ pattern,
137
+ cwd: base,
138
+ backend,
139
+ ignore: ignoreList,
140
+ count: result.length,
141
+ truncated,
142
+ files: result,
143
+ };
144
+ }
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // Express router factory
148
+ // ---------------------------------------------------------------------------
149
+
150
+ export function buildGlobRouter(express) {
151
+ const router = express.Router();
152
+
153
+ async function handle(params, res) {
154
+ try {
155
+ res.json(await globFiles(params));
156
+ } catch (e) {
157
+ res.status(400).json({ error: e.message });
158
+ }
159
+ }
160
+
161
+ router.post("/", (req, res) => handle(req.body || {}, res));
162
+ router.get("/", (req, res) => handle(req.query, res));
163
+
164
+ return router;
165
+ }
@@ -0,0 +1,218 @@
1
+ // daemon/tools/grep.js
2
+ // Native Grep tool for APX — searches file contents by regex pattern.
3
+ // Tries ripgrep (rg) first for speed, falls back to pure Node.js walk.
4
+ //
5
+ // Endpoint: POST /tools/grep body: { pattern, path?, glob?, case_sensitive?, context?, limit? }
6
+ // GET /tools/grep?pattern=...&path=...
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { execFile } from "node:child_process";
11
+ import { promisify } from "node:util";
12
+
13
+ const execFileAsync = promisify(execFile);
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // ripgrep backend
17
+ // ---------------------------------------------------------------------------
18
+
19
+ async function grepWithRg({ pattern, searchPath, glob, caseSensitive, context = 0, limit }) {
20
+ const args = [
21
+ "--json",
22
+ "--max-count", "1", // max matches per file
23
+ "--max-filesize", "1M",
24
+ "-l", // list matching files first? No, we want content
25
+ ];
26
+ // Actually use full output mode
27
+ const fullArgs = [
28
+ "--json",
29
+ "--max-filesize", "1M",
30
+ "-n", // line numbers
31
+ ];
32
+ if (!caseSensitive) fullArgs.push("-i");
33
+ if (context > 0) fullArgs.push("-C", String(context));
34
+ if (glob) fullArgs.push("--glob", glob);
35
+ fullArgs.push(pattern, searchPath);
36
+
37
+ const { stdout } = await execFileAsync("rg", fullArgs, {
38
+ maxBuffer: 8 * 1024 * 1024,
39
+ timeout: 15000,
40
+ });
41
+
42
+ const matches = [];
43
+ let currentFile = null;
44
+
45
+ for (const line of stdout.trim().split("\n")) {
46
+ if (!line) continue;
47
+ let obj;
48
+ try { obj = JSON.parse(line); } catch { continue; }
49
+
50
+ if (obj.type === "match") {
51
+ const file = obj.data?.path?.text || currentFile;
52
+ currentFile = file;
53
+ const lineNum = obj.data?.line_number;
54
+ const text = obj.data?.lines?.text?.replace(/\n$/, "") || "";
55
+ const last = matches[matches.length - 1];
56
+ if (last && last.file === file) {
57
+ last.matches.push({ line: lineNum, text });
58
+ } else {
59
+ matches.push({ file: path.relative(searchPath, file), absolute: file, matches: [{ line: lineNum, text }] });
60
+ }
61
+ if (matches.reduce((s, m) => s + m.matches.length, 0) >= limit) break;
62
+ }
63
+ }
64
+ return { backend: "rg", matches };
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Pure Node.js fallback
69
+ // ---------------------------------------------------------------------------
70
+
71
+ function globToRegex(g) {
72
+ return new RegExp(
73
+ "^" + g
74
+ .replace(/[.+^${}()|[\]\\]/g, "\\$&")
75
+ .replace(/\*\*\//g, "(?:.+/)?")
76
+ .replace(/\*\*/g, ".*")
77
+ .replace(/\*/g, "[^/]*")
78
+ .replace(/\?/g, "[^/]") +
79
+ "$"
80
+ );
81
+ }
82
+
83
+ function walkFiles(dir, { globPattern, dot = false, maxFiles = 2000 } = {}) {
84
+ const globRe = globPattern ? globToRegex(globPattern) : null;
85
+ const result = [];
86
+ const queue = [dir];
87
+ while (queue.length && result.length < maxFiles) {
88
+ const current = queue.shift();
89
+ let entries;
90
+ try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
91
+ for (const e of entries) {
92
+ if (!dot && e.name.startsWith(".")) continue;
93
+ const full = path.join(current, e.name);
94
+ if (e.isDirectory()) { queue.push(full); continue; }
95
+ if (!e.isFile()) continue;
96
+ const rel = path.relative(dir, full);
97
+ if (globRe && !globRe.test(rel) && !globRe.test(e.name)) continue;
98
+ result.push(full);
99
+ }
100
+ }
101
+ return result;
102
+ }
103
+
104
+ async function grepWithNode({ pattern, searchPath, glob, caseSensitive, context = 0, limit }) {
105
+ const re = new RegExp(pattern, caseSensitive ? "" : "i");
106
+ const files = walkFiles(searchPath, { globPattern: glob, dot: false });
107
+ const matches = [];
108
+ let totalMatches = 0;
109
+
110
+ for (const file of files) {
111
+ if (totalMatches >= limit) break;
112
+ let content;
113
+ try { content = fs.readFileSync(file, "utf8"); } catch { continue; }
114
+ const lines = content.split("\n");
115
+ const fileMatches = [];
116
+
117
+ for (let i = 0; i < lines.length; i++) {
118
+ if (!re.test(lines[i])) continue;
119
+ const start = Math.max(0, i - context);
120
+ const end = Math.min(lines.length - 1, i + context);
121
+ const contextLines = [];
122
+ for (let j = start; j <= end; j++) {
123
+ contextLines.push({ line: j + 1, text: lines[j], is_match: j === i });
124
+ }
125
+ fileMatches.push({ line: i + 1, text: lines[i], context: context > 0 ? contextLines : undefined });
126
+ totalMatches++;
127
+ if (totalMatches >= limit) break;
128
+ }
129
+
130
+ if (fileMatches.length > 0) {
131
+ matches.push({
132
+ file: path.relative(searchPath, file),
133
+ absolute: file,
134
+ matches: fileMatches,
135
+ });
136
+ }
137
+ }
138
+ return { backend: "node", matches };
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------
142
+ // Main dispatcher
143
+ // ---------------------------------------------------------------------------
144
+
145
+ export async function grepFiles({
146
+ pattern,
147
+ path: searchPathArg,
148
+ glob,
149
+ case_sensitive = false,
150
+ context = 0,
151
+ limit = 100,
152
+ cwd,
153
+ }) {
154
+ if (!pattern) throw new Error("pattern required");
155
+ const basePath = path.resolve(searchPathArg || cwd || process.cwd());
156
+ if (!fs.existsSync(basePath)) throw new Error(`path does not exist: ${basePath}`);
157
+
158
+ const lim = Math.min(parseInt(limit, 10) || 100, 1000);
159
+ const ctx = Math.min(parseInt(context, 10) || 0, 10);
160
+
161
+ // Validate the regex
162
+ try { new RegExp(pattern); } catch (e) { throw new Error(`invalid regex: ${e.message}`); }
163
+
164
+ let result;
165
+ try {
166
+ result = await grepWithRg({
167
+ pattern,
168
+ searchPath: basePath,
169
+ glob,
170
+ caseSensitive: !!case_sensitive,
171
+ context: ctx,
172
+ limit: lim,
173
+ });
174
+ } catch {
175
+ result = await grepWithNode({
176
+ pattern,
177
+ searchPath: basePath,
178
+ glob,
179
+ caseSensitive: !!case_sensitive,
180
+ context: ctx,
181
+ limit: lim,
182
+ });
183
+ }
184
+
185
+ const totalMatches = result.matches.reduce((s, m) => s + m.matches.length, 0);
186
+ return {
187
+ pattern,
188
+ path: basePath,
189
+ glob: glob || null,
190
+ case_sensitive: !!case_sensitive,
191
+ backend: result.backend,
192
+ files_with_matches: result.matches.length,
193
+ total_matches: totalMatches,
194
+ truncated: totalMatches >= lim,
195
+ matches: result.matches,
196
+ };
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Express router factory
201
+ // ---------------------------------------------------------------------------
202
+
203
+ export function buildGrepRouter(express) {
204
+ const router = express.Router();
205
+
206
+ async function handle(params, res) {
207
+ try {
208
+ res.json(await grepFiles(params));
209
+ } catch (e) {
210
+ res.status(400).json({ error: e.message });
211
+ }
212
+ }
213
+
214
+ router.post("/", (req, res) => handle(req.body || {}, res));
215
+ router.get("/", (req, res) => handle(req.query, res));
216
+
217
+ return router;
218
+ }