EXTENSION seed
codebase
A codebase becomes a tree. Directories become nodes. Files become notes. The AI at /myproject/src/components/Button knows Button's code, its children, its siblings, its tests. Navigate somewhere and the AI thinks about that code from that position. Six modes. Each is a specialist. Chains replace one big loop. code-analyze reads and understands. code-search triangulates across the codebase with five strategies and convergence scoring. code-plan designs the change. code-edit applies diffs. code-test runs and parses tests. code-review checks the work. A cheap model handles search and test. A quality model handles planning and editing. Total cost for a bug fix: less than one large model call. Once code is in the tree, the intelligence bundle works on it automatically. Understanding compresses modules. Scout searches across files. Explore drills into unfamiliar code. Codebook tracks recurring patterns. Evolve detects gaps. Inner monologue thinks about the codebase on breath cycles. Automate flows run CI, code review, and documentation generation on schedule. The tree watches its own codebase while you sleep.
v1.0.2 by TreeOS Site 0 downloads 11 files 1,215 lines 39.2 KB published 38d ago
treeos ext install codebase
View changelog

Manifest

Provides

  • routes
  • tools
  • 1 CLI commands

Requires

  • services: hooks, llm, metadata, tree
  • models: Node, Note

Optional

  • extensions: shell, embed, understanding, scout, explore, treeos-base
SHA256: 1e56137fab550faa09a1e678b1d7ba2d414a7b8fd89ddd84b7969061e2de10b3

CLI Commands

CommandMethodDescription
code [action] [args...]GETCode operations. Actions: ingest <path>, search <query>, status, git-status, git-diff, git-log.

Hooks

Listens To

  • enrichContext

Source Code

1/**
2 * Code Core
3 *
4 * Ingests a codebase into a tree. Directories become nodes. Files become notes.
5 * Respects .gitignore. Skips binaries and node_modules.
6 */
7
8import fs from "fs/promises";
9import path from "path";
10import os from "os";
11import log from "../../seed/log.js";
12import Node from "../../seed/models/node.js";
13import Note from "../../seed/models/note.js";
14import { v4 as uuidv4 } from "uuid";
15import { execFile } from "child_process";
16import { promisify } from "util";
17
18const execFileAsync = promisify(execFile);
19
20let _metadata = null;
21export function configure({ metadata }) { _metadata = metadata; }
22
23// Files/dirs to always skip
24const SKIP_DIRS = new Set([
25  "node_modules", ".git", ".svn", ".hg", "__pycache__", ".next", ".nuxt",
26  "dist", "build", ".cache", "coverage", ".nyc_output", ".tox", "venv",
27  "env", ".env", ".venv", "vendor", "target", "out", ".gradle", ".idea",
28  ".vscode", ".DS_Store",
29]);
30
31const SKIP_EXTENSIONS = new Set([
32  ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico", ".svg", ".webp",
33  ".mp3", ".mp4", ".wav", ".avi", ".mov", ".mkv",
34  ".zip", ".tar", ".gz", ".bz2", ".rar", ".7z",
35  ".exe", ".dll", ".so", ".dylib", ".bin", ".o", ".a",
36  ".woff", ".woff2", ".ttf", ".eot",
37  ".pdf", ".doc", ".docx", ".xls", ".xlsx",
38  ".lock", ".map",
39]);
40
41const MAX_FILE_LINES = 500;
42const MAX_FILE_SIZE = 100000; // 100KB
43const MAX_FILES = 500;
44const MAX_DEPTH = 15;
45
46/**
47 * Parse .gitignore patterns into a simple matcher.
48 */
49async function loadGitignore(repoPath) {
50  const patterns = [];
51  try {
52    const content = await fs.readFile(path.join(repoPath, ".gitignore"), "utf-8");
53    for (const line of content.split("\n")) {
54      const trimmed = line.trim();
55      if (!trimmed || trimmed.startsWith("#")) continue;
56      patterns.push(trimmed);
57    }
58  } catch {}
59  return patterns;
60}
61
62function isIgnored(relativePath, patterns) {
63  const name = path.basename(relativePath);
64  for (const pattern of patterns) {
65    if (pattern === name) return true;
66    if (pattern.endsWith("/") && name === pattern.slice(0, -1)) return true;
67    if (pattern.startsWith("*") && relativePath.endsWith(pattern.slice(1))) return true;
68  }
69  return false;
70}
71
72/**
73 * Detect language from file extension.
74 */
75function detectLanguage(filePath) {
76  const ext = path.extname(filePath).toLowerCase();
77  const map = {
78    ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
79    ".ts": "typescript", ".tsx": "typescript",
80    ".py": "python", ".rb": "ruby", ".go": "go", ".rs": "rust",
81    ".java": "java", ".kt": "kotlin", ".scala": "scala",
82    ".c": "c", ".cpp": "cpp", ".h": "c", ".hpp": "cpp",
83    ".cs": "csharp", ".swift": "swift", ".m": "objc",
84    ".php": "php", ".lua": "lua", ".r": "r",
85    ".sh": "shell", ".bash": "shell", ".zsh": "shell",
86    ".sql": "sql", ".graphql": "graphql",
87    ".html": "html", ".css": "css", ".scss": "scss", ".less": "less",
88    ".json": "json", ".yaml": "yaml", ".yml": "yaml", ".toml": "toml",
89    ".xml": "xml", ".md": "markdown", ".txt": "text",
90    ".dockerfile": "docker", ".env": "env",
91  };
92  return map[ext] || null;
93}
94
95/**
96 * Clone a git repository to a temp directory.
97 * Returns the local path. Caller is responsible for cleanup.
98 */
99export async function cloneRepo(url) {
100  const tmpDir = path.join(os.tmpdir(), `treeos-code-${uuidv4().slice(0, 8)}`);
101  await fs.mkdir(tmpDir, { recursive: true });
102
103  try {
104    await execFileAsync("git", ["clone", "--depth", "1", url, tmpDir], {
105      timeout: 120000,
106      maxBuffer: 1024 * 1024,
107    });
108    log.verbose("Code", `Cloned ${url} to ${tmpDir}`);
109    return tmpDir;
110  } catch (err) {
111    // Clean up on failure
112    await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
113    throw new Error(`Clone failed: ${err.message}`);
114  }
115}
116
117/**
118 * Check if a string looks like a git URL.
119 */
120export function isGitUrl(str) {
121  return /^https?:\/\/.+\.git$|^https?:\/\/github\.com\/|^git@/.test(str);
122}
123
124/**
125 * Ingest a directory or git URL into a tree node.
126 * Creates child nodes for directories, notes for files.
127 */
128export async function ingest(parentNodeId, dirPath, userId, opts = {}) {
129  const repoRoot = opts.repoRoot || dirPath;
130  const gitignore = opts.gitignore || await loadGitignore(repoRoot);
131  const depth = opts.depth || 0;
132  const stats = opts.stats || { files: 0, dirs: 0, lines: 0, skipped: 0 };
133
134  if (depth > MAX_DEPTH) return stats;
135  if (stats.files >= MAX_FILES) return stats;
136
137  let entries;
138  try {
139    entries = await fs.readdir(dirPath, { withFileTypes: true });
140  } catch (err) {
141    log.debug("Code", `Cannot read ${dirPath}: ${err.message}`);
142    return stats;
143  }
144
145  // Sort: directories first, then files
146  entries.sort((a, b) => {
147    if (a.isDirectory() && !b.isDirectory()) return -1;
148    if (!a.isDirectory() && b.isDirectory()) return 1;
149    return a.name.localeCompare(b.name);
150  });
151
152  for (const entry of entries) {
153    if (stats.files >= MAX_FILES) break;
154
155    const entryPath = path.join(dirPath, entry.name);
156    const relativePath = path.relative(repoRoot, entryPath);
157
158    // Skip ignored
159    if (SKIP_DIRS.has(entry.name)) { stats.skipped++; continue; }
160    if (isIgnored(relativePath, gitignore)) { stats.skipped++; continue; }
161
162    if (entry.isDirectory()) {
163      // Create a node for the directory
164      const dirNode = await createCodeNode(parentNodeId, entry.name, userId, "directory");
165      stats.dirs++;
166
167      // Recurse
168      await ingest(String(dirNode._id), entryPath, userId, {
169        repoRoot, gitignore, depth: depth + 1, stats,
170      });
171    } else if (entry.isFile()) {
172      const ext = path.extname(entry.name).toLowerCase();
173      if (SKIP_EXTENSIONS.has(ext)) { stats.skipped++; continue; }
174
175      // Read file
176      try {
177        const fileStat = await fs.stat(entryPath);
178        if (fileStat.size > MAX_FILE_SIZE) {
179          stats.skipped++;
180          continue;
181        }
182
183        const content = await fs.readFile(entryPath, "utf-8");
184        const lines = content.split("\n");
185        const language = detectLanguage(entry.name);
186
187        // Truncate large files
188        const truncated = lines.length > MAX_FILE_LINES;
189        const fileContent = truncated
190          ? lines.slice(0, MAX_FILE_LINES).join("\n") + `\n\n... (${lines.length - MAX_FILE_LINES} more lines truncated)`
191          : content;
192
193        // Write file content directly to Note model.
194        // Bypasses createNote's text limit, hooks, and validation.
195        // Code files can be large. No hooks needed for bulk ingestion.
196        await Note.create({
197          _id: uuidv4(),
198          contentType: "text",
199          content: `// ${entry.name}\n${fileContent}`,
200          userId,
201          nodeId: parentNodeId,
202          wasAi: false,
203          metadata: { code: { fileName: entry.name, language, lines: lines.length, truncated } },
204        });
205
206        stats.files++;
207        stats.lines += lines.length;
208      } catch (err) {
209        log.debug("Code", `Cannot read file ${entryPath}: ${err.message}`);
210        stats.skipped++;
211      }
212    }
213  }
214
215  return stats;
216}
217
218/**
219 * Create a child node with code metadata.
220 * Uses the kernel's createNode for proper hook firing and parent linkage.
221 */
222async function createCodeNode(parentId, name, userId, role) {
223  const { createNode: kernelCreateNode } = await import("../../seed/tree/treeManagement.js");
224
225  // Check if already exists (idempotent re-ingestion)
226  const existing = await Node.findOne({ parent: parentId, name }).select("_id").lean();
227  if (existing) return existing;
228
229  const node = await kernelCreateNode({
230    name,
231    parentId,
232    userId,
233    metadata: { code: { role } },
234  });
235  return node;
236}
237
238/**
239 * Search for a pattern across all file notes in a code tree.
240 */
241export async function searchCode(rootId, query, type = "text") {
242  const results = [];
243  const regex = new RegExp(query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
244
245  // BFS: collect all nodes in the tree
246  const queue = [rootId];
247  const visited = new Set();
248  const Note = (await import("../../seed/models/note.js")).default;
249
250  while (queue.length > 0 && results.length < 20) {
251    const batch = queue.splice(0, 50);
252    const nodes = await Node.find({ parent: { $in: batch } })
253      .select("_id name children metadata")
254      .lean();
255
256    for (const node of nodes) {
257      const id = String(node._id);
258      if (visited.has(id)) continue;
259      visited.add(id);
260      if (node.children?.length) queue.push(id);
261
262      // Search notes on this node
263      const notes = await Note.find({ nodeId: id, contentType: "text" })
264        .select("content")
265        .lean();
266
267      for (const note of notes) {
268        if (!note.content) continue;
269        const matches = note.content.match(regex);
270        if (!matches) continue;
271
272        // Extract context around first match
273        const idx = note.content.search(regex);
274        const start = Math.max(0, idx - 100);
275        const end = Math.min(note.content.length, idx + query.length + 100);
276        const context = note.content.slice(start, end);
277
278        // Get file name from note content (first line is // filename)
279        const firstLine = note.content.split("\n")[0];
280        const fileName = firstLine.startsWith("// ") ? firstLine.slice(3) : null;
281
282        results.push({
283          nodeId: id,
284          nodeName: node.name,
285          fileName,
286          matchCount: matches.length,
287          context: context.trim(),
288        });
289      }
290
291      if (visited.size > 500) break;
292    }
293  }
294
295  return results;
296}
297
298/**
299 * Get code status for a tree.
300 */
301export async function getStatus(rootId) {
302  const root = await Node.findById(rootId).select("metadata").lean();
303  if (!root) return null;
304  const meta = root.metadata instanceof Map ? root.metadata.get("codebase") : root.metadata?.code;
305  return meta || null;
306}
307
1import log from "../../seed/log.js";
2import { configure } from "./core.js";
3import getTools from "./tools.js";
4
5import analyzeMode from "./modes/analyze.js";
6import browseMode from "./modes/browse.js";
7import editMode from "./modes/edit.js";
8import testMode from "./modes/test.js";
9import reviewMode from "./modes/review.js";
10
11export async function init(core) {
12  configure({ metadata: core.metadata });
13
14  // Register LLM slots: different models for different jobs
15  core.llm.registerRootLlmSlot?.("code-analyze");
16  core.llm.registerRootLlmSlot?.("code-search");
17  core.llm.registerRootLlmSlot?.("code-edit");
18  core.llm.registerRootLlmSlot?.("code-test");
19  core.llm.registerRootLlmSlot?.("code-review");
20
21  // Register modes
22  core.modes.registerMode("tree:code-analyze", analyzeMode, "codebase");
23  core.modes.registerMode("tree:code-browse", browseMode, "codebase");
24  core.modes.registerMode("tree:code-edit", editMode, "codebase");
25  core.modes.registerMode("tree:code-test", testMode, "codebase");
26  core.modes.registerMode("tree:code-review", reviewMode, "codebase");
27
28  // LLM slot assignments: cheap for search/test, quality for edit/review
29  if (core.llm.registerModeAssignment) {
30    core.llm.registerModeAssignment("tree:code-analyze", "code-analyze");
31    core.llm.registerModeAssignment("tree:code-browse", "code-analyze");
32    core.llm.registerModeAssignment("tree:code-edit", "code-edit");
33    core.llm.registerModeAssignment("tree:code-test", "code-test");
34    core.llm.registerModeAssignment("tree:code-review", "code-review");
35  }
36
37  // enrichContext: inject code metadata when at a code node
38  core.hooks.register("enrichContext", async ({ context, node, meta }) => {
39    const codeMeta = meta?.code;
40    if (!codeMeta) return;
41
42    context.code = {
43      role: codeMeta.role || (codeMeta.initialized ? "repository" : null),
44      language: codeMeta.language || null,
45      fileCount: codeMeta.fileCount || null,
46      path: codeMeta.path || null,
47    };
48  }, "codebase");
49
50  // Set modes.respond on code roots so the routing index picks them up
51  // This happens at ingest time via the tool, not here.
52
53  log.info("Code", "Loaded. The tree reads code.");
54
55  return {
56    tools: getTools(),
57    // Inject code tools into converse and librarian modes so code-ingest
58    // is available before a code tree is set up.
59    modeTools: [
60      { modeKey: "tree:converse", toolNames: ["code-ingest", "code-search", "code-git"] },
61      { modeKey: "tree:librarian", toolNames: ["code-ingest", "code-search"] },
62    ],
63  };
64}
65
1export default {
2  name: "codebase",
3  version: "1.0.2",
4  builtFor: "seed",
5  scope: "confined",
6  description:
7    "A codebase becomes a tree. Directories become nodes. Files become notes. " +
8    "The AI at /myproject/src/components/Button knows Button's code, its children, " +
9    "its siblings, its tests. Navigate somewhere and the AI thinks about that code " +
10    "from that position. " +
11    "\n\n" +
12    "Six modes. Each is a specialist. Chains replace one big loop. " +
13    "code-analyze reads and understands. code-search triangulates across the codebase " +
14    "with five strategies and convergence scoring. code-plan designs the change. " +
15    "code-edit applies diffs. code-test runs and parses tests. code-review checks " +
16    "the work. A cheap model handles search and test. A quality model handles planning " +
17    "and editing. Total cost for a bug fix: less than one large model call. " +
18    "\n\n" +
19    "Once code is in the tree, the intelligence bundle works on it automatically. " +
20    "Understanding compresses modules. Scout searches across files. Explore drills " +
21    "into unfamiliar code. Codebook tracks recurring patterns. Evolve detects gaps. " +
22    "Inner monologue thinks about the codebase on breath cycles. " +
23    "\n\n" +
24    "Automate flows run CI, code review, and documentation generation on schedule. " +
25    "The tree watches its own codebase while you sleep.",
26
27  territory: "source code, programming, bugs, refactoring, repositories",
28  classifierHints: [
29    /\b(code|codebase|repository|repo|source code|source file)\b/i,
30    /\b(function|class|module|import|export|require)\b/i,
31    /\b(bug|fix|refactor|lint|test|compile|build)\b/i,
32    /\b(ingest|analyze|diff|blame|commit)\b/i,
33  ],
34
35  needs: {
36    services: ["hooks", "llm", "metadata", "tree"],
37    models: ["Node", "Note"],
38  },
39
40  optional: {
41    extensions: ["shell", "embed", "understanding", "scout", "explore", "treeos-base"],
42  },
43
44  provides: {
45    models: {},
46    routes: "./routes.js",
47    tools: true,
48    jobs: false,
49    orchestrator: false,
50    energyActions: {},
51    sessionTypes: {},
52    env: [],
53    cli: [
54      {
55        command: "code [action] [args...]",
56        scope: ["tree"],
57        description: "Code operations. Actions: ingest <path>, search <query>, status, git-status, git-diff, git-log.",
58        method: "GET",
59        endpoint: "/code/status?nodeId=:nodeId",
60      },
61    ],
62
63    hooks: {
64      fires: [],
65      listens: ["enrichContext"],
66    },
67  },
68};
69
1export default {
2  name: "tree:code-analyze",
3  emoji: "๐Ÿ”",
4  label: "Code Analyze",
5  bigMode: "tree",
6  hidden: true,
7
8  toolNames: [
9    "get-node-notes",
10    "get-tree-context",
11    "navigate-tree",
12    "code-search",
13  ],
14
15  buildSystemPrompt({ username, currentNodeId }) {
16    return `You are analyzing code for ${username}.
17
18Your job: read the file content (notes on this node) and the directory structure (children).
19Understand what this code does. Identify dependencies, exports, patterns, edge cases.
20
21Output a clear analysis:
22- What this file/module does (one sentence)
23- Key functions/classes and what they do
24- Dependencies (imports from other modules)
25- Potential issues or edge cases
26- How it connects to the broader codebase
27
28Be specific. Reference actual function names, variable names, line patterns.
29Do not give generic advice. Read the code and tell me what it says.`.trim();
30  },
31};
32
1import { getContextForAi } from "../../../seed/tree/treeFetch.js";
2
3export default {
4  name: "tree:code-browse",
5  emoji: "๐Ÿ“‚",
6  label: "Code Browse",
7  bigMode: "tree",
8
9  maxMessagesBeforeLoop: 20,
10  preserveContextOnLoop: true,
11
12  toolNames: [
13    "navigate-tree",
14    "get-tree-context",
15    "get-node-notes",
16    "code-search",
17    "code-git",
18    "code-ingest",
19    "code-sandbox",
20    "code-test",
21  ],
22
23  async buildSystemPrompt({ username, rootId, currentNodeId, conversationMemory }) {
24    const nodeId = currentNodeId || rootId;
25
26    let context = null;
27    if (nodeId) {
28      try {
29        context = await getContextForAi(nodeId, {
30          includeNotes: true,
31          includeChildren: true,
32          includeParentChain: true,
33          userId: null,
34        });
35      } catch {}
36    }
37
38    const ctx = context || {};
39    const name = ctx.name || "this module";
40
41    const sections = [];
42
43    if (ctx.notes?.length) {
44      const fileContent = ctx.notes
45        .slice(0, 10)
46        .map(n => n.content || "")
47        .filter(c => c.length > 0)
48        .join("\n---\n");
49      if (fileContent) sections.push(`SOURCE CODE:\n${fileContent}`);
50    }
51
52    if (ctx.children?.length) {
53      sections.push(`CONTENTS: ${ctx.children.map(c => c.name).join(", ")}`);
54    }
55
56    if (ctx.parentChain?.length) {
57      sections.push(`PATH: ${ctx.parentChain.map(p => p.name).join("/")}`);
58    }
59
60    if (conversationMemory) {
61      sections.push(`RECENT CONVERSATION:\n${conversationMemory}`);
62    }
63
64    const contextBlock = sections.length > 0
65      ? sections.join("\n\n")
66      : "This module is empty. No files yet.";
67
68    return `You are ${username}'s code assistant at "${name}".
69
70${contextBlock}
71
72You have read the code at this position. You know what's here.
73
74WHAT YOU DO:
75- Explain code. Answer questions about it. Trace logic.
76- Search across the codebase with code-search.
77- Navigate to related files with navigate-tree.
78- Check git state with code-git.
79- Reference specific function names, line patterns, variable names.
80
81HOW YOU SPEAK:
82- You are not a generic assistant. You are a developer who read this code.
83- Be specific. "fetchUser on line 23 returns a Promise that resolves to..." not "this file has some API calls."
84- If you don't know, search. Don't guess.`.trim();
85  },
86};
87
1export default {
2  name: "tree:code-edit",
3  emoji: "โœ๏ธ",
4  label: "Code Edit",
5  bigMode: "tree",
6  hidden: true,
7
8  maxMessagesBeforeLoop: 20,
9  preserveContextOnLoop: true,
10
11  toolNames: [
12    "navigate-tree",
13    "get-tree-context",
14    "get-node-notes",
15    "code-search",
16    "create-node-note",
17    "edit-node-note",
18    "code-sandbox",
19    "code-test",
20    "code-run-file",
21  ],
22
23  buildSystemPrompt({ username }) {
24    return `You are editing code for ${username}.
25
26You have a plan from the previous step. Execute it precisely.
27
28RULES:
29- Make targeted edits. Don't rewrite entire files.
30- Use edit-node-note to modify existing file content.
31- Use create-node-note to add new files.
32- After editing, confirm what you changed and why.
33- One edit at a time. Verify before moving to the next.`.trim();
34  },
35};
36
1export default {
2  name: "tree:code-review",
3  emoji: "๐Ÿ‘๏ธ",
4  label: "Code Review",
5  bigMode: "tree",
6  hidden: true,
7
8  toolNames: [
9    "code-git",
10    "get-node-notes",
11    "code-search",
12  ],
13
14  buildSystemPrompt({ username }) {
15    return `You are reviewing code changes for ${username}.
16
17Read the diff. Check for:
18- Logic errors or edge cases
19- Missing error handling
20- Breaking changes to public APIs
21- Naming inconsistencies
22- Security concerns (injection, exposure, auth bypass)
23- Performance issues (N+1 queries, unbounded loops, memory leaks)
24
25Be specific. Reference the actual code. "Line 23 catches the error but doesn't log it" not "consider adding error handling."
26
27If the change looks good, say so and explain why. Don't invent problems.`.trim();
28  },
29};
30
1export default {
2  name: "tree:code-test",
3  emoji: "๐Ÿงช",
4  label: "Code Test",
5  bigMode: "tree",
6  hidden: true,
7
8  toolNames: [
9    "code-run",
10    "code-test",
11    "code-run-file",
12    "code-sandbox",
13    "code-search",
14  ],
15
16  buildSystemPrompt({ username }) {
17    return `You are running tests for ${username}.
18
19Run the test command. Parse the results.
20
21OUTPUT (structured):
22- Total tests, passed, failed
23- For each failure: test name, file, error message
24- Summary: what broke and likely root cause
25
26If all tests pass, say so briefly.
27If tests fail, identify the failing files so the next step can navigate to them.`.trim();
28  },
29};
30
1import express from "express";
2import authenticate from "../../seed/middleware/authenticate.js";
3import { sendOk, sendError, ERR } from "../../seed/protocol.js";
4import { ingest, getStatus, searchCode } from "./core.js";
5
6const router = express.Router();
7
8// POST /code/ingest - ingest a codebase
9router.post("/code/ingest", authenticate, async (req, res) => {
10  try {
11    const { rootId, path: dirPath, ignore } = req.body;
12    if (!rootId || !dirPath) return sendError(res, 400, ERR.INVALID_INPUT, "rootId and path required");
13
14    const stats = await ingest(rootId, dirPath, req.userId, { ignore });
15
16    const Node = (await import("../../seed/models/node.js")).default;
17    await Node.updateOne({ _id: rootId }, {
18      $set: {
19        "metadata.code": {
20          initialized: true,
21          ingestedAt: new Date().toISOString(),
22          path: dirPath,
23          fileCount: stats.files,
24          dirCount: stats.dirs,
25          totalLines: stats.lines,
26        },
27        "metadata.modes.respond": "tree:code-browse",
28      },
29    });
30
31    sendOk(res, { stats });
32  } catch (err) {
33    sendError(res, 500, ERR.INTERNAL, err.message);
34  }
35});
36
37// GET /code/status - code tree status
38router.get("/code/status", authenticate, async (req, res) => {
39  try {
40    const nodeId = req.query.nodeId || req.query.rootId;
41    if (!nodeId) return sendError(res, 400, ERR.INVALID_INPUT, "nodeId required");
42    const status = await getStatus(nodeId);
43    sendOk(res, status || { initialized: false });
44  } catch (err) {
45    sendError(res, 500, ERR.INTERNAL, err.message);
46  }
47});
48
49// GET /code/search - search codebase
50router.get("/code/search", authenticate, async (req, res) => {
51  try {
52    const { rootId, query } = req.query;
53    if (!rootId || !query) return sendError(res, 400, ERR.INVALID_INPUT, "rootId and query required");
54    const results = await searchCode(rootId, query);
55    sendOk(res, { results });
56  } catch (err) {
57    sendError(res, 500, ERR.INTERNAL, err.message);
58  }
59});
60
61export default router;
62
1/**
2 * Code Sandbox
3 *
4 * Runs code in an isolated child process. No network. Memory limited.
5 * Timeout enforced. The AI writes code, the sandbox runs it, results come back.
6 *
7 * Two modes:
8 * 1. Run a file: execute a file in the repo context (tests, scripts)
9 * 2. Run snippet: execute arbitrary code the AI wrote (validation, experiments)
10 */
11
12import { execFile, spawn } from "child_process";
13import { promisify } from "util";
14import fs from "fs/promises";
15import path from "path";
16import os from "os";
17import { v4 as uuidv4 } from "uuid";
18import log from "../../seed/log.js";
19
20const execFileAsync = promisify(execFile);
21
22const TIMEOUT_MS = 30000;
23const MAX_OUTPUT = 16384; // 16KB
24const MAX_MEMORY_MB = 128;
25
26// Blocked patterns in snippets
27const BLOCKED = [
28  /require\s*\(\s*['"]child_process['"]\s*\)/,
29  /require\s*\(\s*['"]fs['"]\s*\)/,
30  /require\s*\(\s*['"]net['"]\s*\)/,
31  /require\s*\(\s*['"]http['"]\s*\)/,
32  /require\s*\(\s*['"]https['"]\s*\)/,
33  /import\s+.*from\s+['"]child_process['"]/,
34  /import\s+.*from\s+['"]net['"]/,
35  /import\s+.*from\s+['"]http['"]/,
36  /import\s+.*from\s+['"]https['"]/,
37  /process\.exit/,
38  /process\.kill/,
39  /process\.env/,
40  /eval\s*\(/,
41  /Function\s*\(/,
42];
43
44/**
45 * Check if a code snippet contains blocked patterns.
46 */
47function validateSnippet(code) {
48  for (const pattern of BLOCKED) {
49    if (pattern.test(code)) {
50      return `Blocked: ${pattern.source}`;
51    }
52  }
53  return null;
54}
55
56/**
57 * Run a code snippet in an isolated child process.
58 * No network, limited memory, timeout enforced.
59 *
60 * @param {string} code - JavaScript code to execute
61 * @param {string} [cwd] - Working directory (repo root)
62 * @returns {{ success: boolean, output: string, error?: string, exitCode: number, durationMs: number }}
63 */
64export async function runSnippet(code, cwd = null) {
65  const violation = validateSnippet(code);
66  if (violation) {
67    return { success: false, output: "", error: violation, exitCode: -1, durationMs: 0 };
68  }
69
70  // Write snippet to a temp file
71  const tmpFile = path.join(os.tmpdir(), `treeos-sandbox-${uuidv4().slice(0, 8)}.mjs`);
72
73  try {
74    await fs.writeFile(tmpFile, code, "utf-8");
75
76    const start = Date.now();
77    const result = await runIsolated(tmpFile, cwd);
78    result.durationMs = Date.now() - start;
79    return result;
80  } finally {
81    await fs.unlink(tmpFile).catch(() => {});
82  }
83}
84
85/**
86 * Run a file from the repo in an isolated child process.
87 *
88 * @param {string} filePath - Absolute path to the file to run
89 * @param {string} [cwd] - Working directory
90 * @returns {{ success: boolean, output: string, error?: string, exitCode: number, durationMs: number }}
91 */
92export async function runFile(filePath, cwd = null) {
93  // Verify file exists
94  try {
95    await fs.access(filePath);
96  } catch {
97    return { success: false, output: "", error: `File not found: ${filePath}`, exitCode: -1, durationMs: 0 };
98  }
99
100  const start = Date.now();
101  const result = await runIsolated(filePath, cwd || path.dirname(filePath));
102  result.durationMs = Date.now() - start;
103  return result;
104}
105
106/**
107 * Run a file in a restricted child process.
108 */
109async function runIsolated(filePath, cwd) {
110  return new Promise((resolve) => {
111    const args = [
112      `--max-old-space-size=${MAX_MEMORY_MB}`,
113      "--no-warnings",
114      filePath,
115    ];
116
117    const env = {
118      // Minimal env: no secrets, no credentials
119      PATH: process.env.PATH,
120      NODE_ENV: "sandbox",
121      HOME: os.tmpdir(),
122    };
123
124    const child = spawn("node", args, {
125      cwd: cwd || os.tmpdir(),
126      env,
127      timeout: TIMEOUT_MS,
128      stdio: ["ignore", "pipe", "pipe"],
129      // No shell. Direct exec. No injection.
130    });
131
132    let stdout = "";
133    let stderr = "";
134
135    child.stdout.on("data", (data) => {
136      if (stdout.length < MAX_OUTPUT) stdout += data.toString();
137    });
138
139    child.stderr.on("data", (data) => {
140      if (stderr.length < MAX_OUTPUT) stderr += data.toString();
141    });
142
143    const timer = setTimeout(() => {
144      child.kill("SIGKILL");
145    }, TIMEOUT_MS);
146
147    child.on("close", (exitCode, signal) => {
148      clearTimeout(timer);
149
150      const timedOut = signal === "SIGKILL";
151      const output = (stdout + (stderr ? "\nSTDERR:\n" + stderr : "")).slice(0, MAX_OUTPUT);
152
153      resolve({
154        success: exitCode === 0 && !timedOut,
155        output: timedOut ? output + "\n(timed out after 30s)" : output,
156        error: timedOut ? "Timed out" : (exitCode !== 0 ? `Exit code ${exitCode}` : null),
157        exitCode: exitCode ?? -1,
158      });
159    });
160
161    child.on("error", (err) => {
162      clearTimeout(timer);
163      resolve({
164        success: false,
165        output: "",
166        error: err.message,
167        exitCode: -1,
168      });
169    });
170  });
171}
172
173/**
174 * Run tests in the repo using the detected test runner.
175 * Tries common test commands in order.
176 *
177 * @param {string} repoPath - Absolute path to the repository
178 * @returns {{ success: boolean, output: string, runner: string, durationMs: number }}
179 */
180export async function runTests(repoPath) {
181  // Detect test runner from package.json
182  let testCommand = null;
183  try {
184    const pkg = JSON.parse(await fs.readFile(path.join(repoPath, "package.json"), "utf-8"));
185    if (pkg.scripts?.test && pkg.scripts.test !== "echo \"Error: no test specified\" && exit 1") {
186      testCommand = "npm test";
187    }
188  } catch {}
189
190  if (!testCommand) {
191    // Try common runners
192    const runners = [
193      { file: "jest.config.js", cmd: "npx jest --no-coverage" },
194      { file: "vitest.config.js", cmd: "npx vitest run" },
195      { file: "pytest.ini", cmd: "pytest" },
196      { file: "Cargo.toml", cmd: "cargo test" },
197      { file: "go.mod", cmd: "go test ./..." },
198    ];
199    for (const r of runners) {
200      try {
201        await fs.access(path.join(repoPath, r.file));
202        testCommand = r.cmd;
203        break;
204      } catch {}
205    }
206  }
207
208  if (!testCommand) {
209    return { success: false, output: "No test runner detected.", runner: null, durationMs: 0 };
210  }
211
212  const parts = testCommand.split(/\s+/);
213  const start = Date.now();
214
215  try {
216    const { stdout, stderr } = await execFileAsync(parts[0], parts.slice(1), {
217      cwd: repoPath,
218      timeout: TIMEOUT_MS * 2, // tests get more time
219      maxBuffer: MAX_OUTPUT * 2,
220      env: { ...process.env, NODE_ENV: "test", CI: "true" },
221    });
222
223    return {
224      success: true,
225      output: (stdout + (stderr ? "\nSTDERR:\n" + stderr : "")).slice(0, MAX_OUTPUT),
226      runner: testCommand,
227      durationMs: Date.now() - start,
228    };
229  } catch (err) {
230    return {
231      success: false,
232      output: ((err.stdout || "") + "\n" + (err.stderr || "")).slice(0, MAX_OUTPUT),
233      runner: testCommand,
234      error: `Exit ${err.code || "error"}`,
235      durationMs: Date.now() - start,
236    };
237  }
238}
239
1import { z } from "zod";
2import { ingest, searchCode, getStatus, cloneRepo, isGitUrl } from "./core.js";
3import { runSnippet, runFile, runTests } from "./sandbox.js";
4import fs from "fs/promises";
5import { execFile } from "child_process";
6import { promisify } from "util";
7
8const execFileAsync = promisify(execFile);
9
10// Safe commands for code-run
11const ALLOWED_COMMANDS = /^(npm test|npm run|npx|node|python|pytest|go test|cargo test|make|gradle|mvn|jest|mocha|vitest|eslint|prettier|tsc|rustfmt|gofmt|black|flake8|rubocop)/;
12const BLOCKED_PATTERNS = /rm\s+-rf|>\s*\/|sudo|chmod|chown|kill|pkill|shutdown|reboot|mkfs|dd\s+if/;
13
14export default function getTools() {
15  return [
16    {
17      name: "code-ingest",
18      description: "Ingest a codebase into the tree. Directories become nodes. Files become notes. Respects .gitignore.",
19      schema: {
20        path: z.string().describe("Local path or git URL (https://github.com/user/repo) to ingest"),
21      },
22      annotations: { readOnlyHint: false },
23      async handler({ path: inputPath, userId, nodeId }) {
24        if (!inputPath) return { content: [{ type: "text", text: "Path or URL required." }] };
25        if (!nodeId) return { content: [{ type: "text", text: "Not in a tree position." }] };
26
27        const Node = (await import("../../seed/models/node.js")).default;
28        const node = await Node.findById(nodeId).select("_id name metadata").lean();
29        if (!node) return { content: [{ type: "text", text: "Node not found." }] };
30
31        let dirPath = inputPath;
32        let clonedDir = null;
33        let source = inputPath;
34
35        // Clone if it's a git URL
36        if (isGitUrl(inputPath)) {
37          try {
38            clonedDir = await cloneRepo(inputPath);
39            dirPath = clonedDir;
40            source = inputPath;
41          } catch (err) {
42            return { content: [{ type: "text", text: `Clone failed: ${err.message}` }] };
43          }
44        }
45
46        try {
47          const stats = await ingest(nodeId, dirPath, userId);
48
49          // Mark root as code-initialized
50          await Node.updateOne({ _id: nodeId }, {
51            $set: {
52              "metadata.codebase": {
53                initialized: true,
54                ingestedAt: new Date().toISOString(),
55                path: dirPath,
56                source,
57                fileCount: stats.files,
58                dirCount: stats.dirs,
59                totalLines: stats.lines,
60                skipped: stats.skipped,
61              },
62              "metadata.modes.respond": "tree:code-browse",
63            },
64          });
65
66          return {
67            content: [{ type: "text", text: `Ingested: ${stats.files} files, ${stats.dirs} directories, ${stats.lines} lines. ${stats.skipped} skipped.` }],
68          };
69        } finally {
70          // Clean up cloned repo
71          if (clonedDir) {
72            await fs.rm(clonedDir, { recursive: true, force: true }).catch(() => {});
73          }
74        }
75      },
76    },
77
78    {
79      name: "code-search",
80      description: "Search for code patterns across the codebase. Returns matching files with context.",
81      schema: {
82        query: z.string().describe("Search query: function name, variable, text pattern"),
83      },
84      annotations: { readOnlyHint: true },
85      async handler({ query, rootId }) {
86        if (!query) return { content: [{ type: "text", text: "Query required." }] };
87        if (!rootId) return { content: [{ type: "text", text: "Not in a tree." }] };
88
89        const results = await searchCode(rootId, query);
90
91        if (results.length === 0) {
92          return { content: [{ type: "text", text: `No matches for "${query}".` }] };
93        }
94
95        const text = results.map(r =>
96          `${r.nodeName}/${r.fileName || "?"} (${r.matchCount} matches)\n  ${r.context}`
97        ).join("\n\n");
98
99        return { content: [{ type: "text", text }] };
100      },
101    },
102
103    {
104      name: "code-git",
105      description: "Read git state: status, diff, log, or blame. Read-only.",
106      schema: {
107        action: z.enum(["status", "diff", "log", "blame"]).describe("Git action"),
108        path: z.string().optional().describe("File path for blame, or repo path"),
109      },
110      annotations: { readOnlyHint: true },
111      async handler({ action, path: filePath, rootId }) {
112        // Find the repo path from the code root metadata
113        const Node = (await import("../../seed/models/node.js")).default;
114        const root = await Node.findById(rootId).select("metadata").lean();
115        const meta = root?.metadata instanceof Map ? root.metadata.get("codebase") : root?.metadata?.code;
116        const repoPath = meta?.path;
117        if (!repoPath) return { content: [{ type: "text", text: "No code repository ingested at this root." }] };
118
119        const commands = {
120          status: ["git", ["status", "--short"]],
121          diff: ["git", ["diff", "--stat"]],
122          log: ["git", ["log", "--oneline", "-20"]],
123          blame: filePath ? ["git", ["blame", "--line-porcelain", filePath]] : null,
124        };
125
126        const cmd = commands[action];
127        if (!cmd) return { content: [{ type: "text", text: `Unknown action: ${action}` }] };
128
129        try {
130          const { stdout } = await execFileAsync(cmd[0], cmd[1], {
131            cwd: repoPath,
132            timeout: 15000,
133            maxBuffer: 1024 * 64,
134          });
135          const output = stdout.slice(0, 8000);
136          return { content: [{ type: "text", text: output || "(empty)" }] };
137        } catch (err) {
138          return { content: [{ type: "text", text: `Git error: ${err.message}` }] };
139        }
140      },
141    },
142
143    {
144      name: "code-run",
145      description: "Run build/test/lint commands in the code repository. Sanitized: only allows safe commands.",
146      schema: {
147        command: z.string().describe("Command to run: npm test, eslint src/, etc."),
148      },
149      annotations: { readOnlyHint: false },
150      async handler({ command, rootId }) {
151        if (!command) return { content: [{ type: "text", text: "Command required." }] };
152        if (!ALLOWED_COMMANDS.test(command)) {
153          return { content: [{ type: "text", text: `Command not allowed. Use: npm test, eslint, prettier, etc.` }] };
154        }
155        if (BLOCKED_PATTERNS.test(command)) {
156          return { content: [{ type: "text", text: "Blocked: dangerous pattern detected." }] };
157        }
158
159        const Node = (await import("../../seed/models/node.js")).default;
160        const root = await Node.findById(rootId).select("metadata").lean();
161        const meta = root?.metadata instanceof Map ? root.metadata.get("codebase") : root?.metadata?.code;
162        const repoPath = meta?.path;
163        if (!repoPath) return { content: [{ type: "text", text: "No code repository ingested at this root." }] };
164
165        const parts = command.split(/\s+/);
166        try {
167          const { stdout, stderr } = await execFileAsync(parts[0], parts.slice(1), {
168            cwd: repoPath,
169            timeout: 30000,
170            maxBuffer: 1024 * 64,
171          });
172          const output = (stdout + (stderr ? "\nSTDERR:\n" + stderr : "")).slice(0, 8000);
173          return { content: [{ type: "text", text: output || "(no output)" }] };
174        } catch (err) {
175          const output = ((err.stdout || "") + "\n" + (err.stderr || "")).slice(0, 8000);
176          return { content: [{ type: "text", text: `Exit ${err.code || "error"}:\n${output}` }] };
177        }
178      },
179    },
180
181    {
182      name: "code-sandbox",
183      description: "Run a JavaScript/Node.js code snippet in an isolated sandbox. No network, no filesystem, no secrets. 30s timeout, 128MB memory. Use for validation, experiments, quick tests.",
184      schema: {
185        code: z.string().describe("JavaScript code to execute"),
186      },
187      annotations: { readOnlyHint: false },
188      async handler({ code, rootId }) {
189        if (!code) return { content: [{ type: "text", text: "Code required." }] };
190
191        const Node = (await import("../../seed/models/node.js")).default;
192        const root = rootId ? await Node.findById(rootId).select("metadata").lean() : null;
193        const meta = root?.metadata instanceof Map ? root.metadata.get("codebase") : root?.metadata?.code;
194        const cwd = meta?.path || null;
195
196        const result = await runSnippet(code, cwd);
197
198        const status = result.success ? "OK" : "FAILED";
199        const text = `[${status}] ${result.durationMs}ms\n${result.output}${result.error ? "\nError: " + result.error : ""}`;
200        return { content: [{ type: "text", text }] };
201      },
202    },
203
204    {
205      name: "code-test",
206      description: "Run the repository's test suite. Auto-detects test runner (npm test, jest, vitest, pytest, cargo test, go test). Returns pass/fail results.",
207      schema: {},
208      annotations: { readOnlyHint: true },
209      async handler({ rootId }) {
210        const Node = (await import("../../seed/models/node.js")).default;
211        const root = await Node.findById(rootId).select("metadata").lean();
212        const meta = root?.metadata instanceof Map ? root.metadata.get("codebase") : root?.metadata?.code;
213        const repoPath = meta?.path;
214        if (!repoPath) return { content: [{ type: "text", text: "No code repository ingested at this root." }] };
215
216        const result = await runTests(repoPath);
217
218        const status = result.success ? "PASS" : "FAIL";
219        const runner = result.runner ? ` (${result.runner})` : "";
220        const text = `[${status}]${runner} ${result.durationMs}ms\n${result.output}${result.error ? "\nError: " + result.error : ""}`;
221        return { content: [{ type: "text", text }] };
222      },
223    },
224
225    {
226      name: "code-run-file",
227      description: "Run a specific file in the sandbox. For test files, scripts, or any executable code file.",
228      schema: {
229        file: z.string().describe("Relative path to the file within the repo (e.g. tests/auth.test.js)"),
230      },
231      annotations: { readOnlyHint: false },
232      async handler({ file, rootId }) {
233        if (!file) return { content: [{ type: "text", text: "File path required." }] };
234
235        const Node = (await import("../../seed/models/node.js")).default;
236        const root = await Node.findById(rootId).select("metadata").lean();
237        const meta = root?.metadata instanceof Map ? root.metadata.get("codebase") : root?.metadata?.code;
238        const repoPath = meta?.path;
239        if (!repoPath) return { content: [{ type: "text", text: "No code repository ingested at this root." }] };
240
241        const { default: path } = await import("path");
242        const fullPath = path.resolve(repoPath, file);
243
244        // Prevent path traversal
245        if (!fullPath.startsWith(repoPath)) {
246          return { content: [{ type: "text", text: "Path traversal blocked." }] };
247        }
248
249        const result = await runFile(fullPath, repoPath);
250
251        const status = result.success ? "OK" : "FAILED";
252        const text = `[${status}] ${result.durationMs}ms\n${result.output}${result.error ? "\nError: " + result.error : ""}`;
253        return { content: [{ type: "text", text }] };
254      },
255    },
256  ];
257}
258

Versions

Version Published Downloads
1.0.2 38d ago 0
1.0.1 38d ago 0
0 stars
0 flags
React from the CLI: treeos ext star codebase

Comments

Loading comments...

Post comments from the CLI: treeos ext comment codebase "your comment"
Max 3 comments per extension. One star and one flag per user.