@agentprojectcontext/apx 1.10.4 → 1.11.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/package.json +5 -1
- package/src/cli/commands/search.js +62 -0
- package/src/cli/index.js +21 -0
- package/src/core/agent-system.js +15 -0
- package/src/daemon/api.js +229 -0
- package/src/daemon/engines/anthropic.js +19 -1
- package/src/daemon/engines/index.js +2 -1
- package/src/daemon/engines/openai.js +22 -2
- package/src/daemon/plugins/telegram.js +248 -2
- package/src/daemon/super-agent.js +42 -1
- package/src/daemon/tools/browser.js +424 -0
- package/src/daemon/tools/fetch.js +138 -0
- package/src/daemon/tools/glob.js +165 -0
- package/src/daemon/tools/grep.js +218 -0
- package/src/daemon/tools/registry.js +729 -0
- package/src/daemon/tools/search.js +290 -0
|
@@ -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
|
+
}
|