feat(infra): port tool system ke TypeScript (37 tools + registry + executor)
Fase 3b rewrite Zesdex dari Rust ke TypeScript/Bun: - Tool interface, ToolCtx + ToolCtxBuilder, InfrastructureToolExecutor - Registry: allTools (37 tool), toolDefs, toolIsRisky, toolIsParallelSafe - FS tools: read, write, edit, delete - Search: grep, glob (glob matcher ringan) - Shell: bash (timeout + timeout_spawn), bash_output, bash_kill - Git: git_operator, git_worktree, git_cred + checkGitDestructive, checkCredentialRead - Memory: remember, forget, recall via MarkdownMemoryRepository - Utilitas: cd, dir_list, dir_cache_update, pong, todowrite, todofinish - Best-practice: best_practice, commit_convention + BestPracticeEngine - Web search (async fetch ke SearXNG), semantic_search/rebuild_index/list_symbols - Workflow/spawn/parallel_delegate (delegasi ke engine 3c/3d dengan placeholder) - Graduated checks + resolve_path sandbox (anti path-escape) - 16 test tool system (registry, guards, resolve_path, fs roundtrip) 54 test hijau (38 lama + 16 baru), tsc strict bersih.
This commit is contained in:
1 parent
a2c7e5d44c
commit
82b51f7921
47 files changed
+3277
-2
No files matched your search
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* Glob: list files matching a glob pattern under a directory root.
|
||||
* Mirrors `tools/search.rs` Glob tool. Uses a lightweight `**`/`*`/`?`
|
||||
* matcher rather than the `globset` crate.
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { JsonValue } from "@zesdex/domain";
|
||||
import { type Tool, type ToolCtx } from "../mod.ts";
|
||||
import { argStr, resolvePath } from "../util.ts";
|
||||
|
||||
/** Convert a glob pattern to a RegExp. Supports `**`, `*`, `?`, `{a,b}`. */
|
||||
export function globToRegExp(pattern: string): RegExp {
|
||||
// Split path separators to handle `**` correctly.
|
||||
const parts = pattern.split("/");
|
||||
let re = "";
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
const part = parts[i] ?? "";
|
||||
if (part === "**") {
|
||||
re += "(?:.*/)?";
|
||||
continue;
|
||||
}
|
||||
if (i > 0) re += "/";
|
||||
let out = "";
|
||||
for (let j = 0; j < part.length; j++) {
|
||||
const ch = part[j];
|
||||
if (ch === "*") out += "[^/]*";
|
||||
else if (ch === "?") out += "[^/]";
|
||||
else if (ch === ".") out += "\\.";
|
||||
else if (ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "+" || ch === "^" || ch === "$") out += `\\${ch}`;
|
||||
else out += ch;
|
||||
}
|
||||
re += out;
|
||||
}
|
||||
return new RegExp(`^${re}$`);
|
||||
}
|
||||
|
||||
/** List files matching a glob pattern. */
|
||||
export class Glob implements Tool {
|
||||
name = "glob";
|
||||
description = "List files matching a glob pattern";
|
||||
|
||||
parameters = {
|
||||
type: "object",
|
||||
properties: {
|
||||
pattern: { type: "string", description: "Glob pattern to match files (e.g. '**/*.rs')" },
|
||||
path: { type: "string", description: "Root path to search from (relative to workspace root)" },
|
||||
},
|
||||
required: ["pattern", "path"],
|
||||
};
|
||||
|
||||
run(ctx: ToolCtx, args: JsonValue): string {
|
||||
const patStr = argStr(args, "pattern");
|
||||
const rel = argStr(args, "path");
|
||||
const root = resolvePath(ctx.workspaces, rel);
|
||||
|
||||
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
||||
throw new Error(`path '${rel}' is not a valid directory`);
|
||||
}
|
||||
|
||||
let re: RegExp;
|
||||
try {
|
||||
re = globToRegExp(patStr);
|
||||
} catch (e) {
|
||||
throw new Error(`invalid glob pattern '${patStr}': ${(e as Error).message}`);
|
||||
}
|
||||
|
||||
const matches: string[] = [];
|
||||
const walk = (dir: string) => {
|
||||
let entries: fs.Dirent[];
|
||||
try {
|
||||
entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "target") continue;
|
||||
walk(full);
|
||||
}
|
||||
const relPath = path.relative(root, full);
|
||||
if (re.test(relPath)) {
|
||||
matches.push(relPath + (entry.isDirectory() ? "/" : ""));
|
||||
}
|
||||
}
|
||||
};
|
||||
walk(root);
|
||||
matches.sort();
|
||||
|
||||
if (matches.length === 0) {
|
||||
return `no files match '${patStr}' in ${rel}`;
|
||||
}
|
||||
return matches.join("\n");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user