Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,37 @@ An interactive web dashboard opens with your codebase visualized as a graph —
/understand src/frontend
```

### Agent-friendly graph CLI

After `/understand` creates `.understand-anything/knowledge-graph.json`, agents and scripts can query the graph without loading the full JSON into context. The installer links `ugraph` for macOS/Linux and creates a Windows command shim; npm-style installs also expose it via the package `bin`.

```bash
# Project metadata, counts, layers, tags, and graph freshness
ugraph overview

# Search nodes by name, path, tags, and summary
ugraph find auth --limit 10

# Show one node and its direct incoming/outgoing relationships
ugraph node src/auth/login.ts

# Expand a token-efficient subgraph around a node
ugraph neighbors src/auth/login.ts --depth 2 --limit 30

# Approximate callers/importers/tests affected by a changed file
ugraph impact src/auth/login.ts --depth 2

# Build compact context for an agent question
ugraph context "payment flow" --limit 25

# Inspect architecture layers, guided tour, or graph staleness
ugraph layers
ugraph tour --nodes
ugraph stale
```

`ugraph` returns compact JSON by default for reliable agent parsing. Use `--pretty`, `--format text`, or `--format md` for human-readable output.

---

## 🌐 Multi-Platform Installation
Expand Down
33 changes: 33 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ $ErrorActionPreference = 'Stop'
$RepoUrl = if ($env:UA_REPO_URL) { $env:UA_REPO_URL } else { 'https://github.com/Egonex-AI/Understand-Anything.git' }
$RepoDir = if ($env:UA_DIR) { $env:UA_DIR } else { Join-Path $HOME '.understand-anything\repo' }
$PluginLink = Join-Path $HOME '.understand-anything-plugin'
$CliBinDir = if ($env:UA_BIN_DIR) { $env:UA_BIN_DIR } else { Join-Path $HOME '.understand-anything\bin' }
$CliCmd = Join-Path $CliBinDir 'ugraph.cmd'

# Platform table — Target = skills directory; Style = "per-skill" | "folder"
$Platforms = [ordered]@{
Expand Down Expand Up @@ -59,6 +61,7 @@ $($Platforms.Keys -join ', ')
Environment:
UA_REPO_URL Override clone URL
UA_DIR Override clone destination (default: %USERPROFILE%\.understand-anything\repo)
UA_BIN_DIR Override CLI shim directory (default: %USERPROFILE%\.understand-anything\bin)
"@
}

Expand Down Expand Up @@ -198,13 +201,42 @@ function ConvertTo-FileUri([string]$Path) {
return 'file:///' + ($Path -replace '\\', '/')
}

function Link-Cli {
$script = Join-Path $RepoDir 'understand-anything-plugin\bin\ugraph.js'
if (-not (Test-Path $script)) {
Write-Host " • ugraph CLI not found at $script, skipping"
return
}

if (-not (Test-Path $CliBinDir)) { New-Item -ItemType Directory -Path $CliBinDir | Out-Null }
$content = "@echo off`r`nnode `"$script`" %*`r`n"
Set-Content -LiteralPath $CliCmd -Value $content -NoNewline -Encoding ASCII
Write-Host " ✓ $CliCmd → $script"

$pathEntries = ($env:PATH -split ';') | Where-Object { $_ }
if ($pathEntries -notcontains $CliBinDir) {
Write-Host " Tip: add $CliBinDir to PATH to run ugraph from any shell."
}
}

function Unlink-Cli {
if (-not (Test-Path $CliCmd)) { return }
$content = Get-Content -LiteralPath $CliCmd -Raw
if ($content -match 'understand-anything-plugin[\\/]+bin[\\/]+ugraph\.js') {
Remove-Item -LiteralPath $CliCmd -Force
Write-Host " ✓ removed $CliCmd"
}
}

function Cmd-Install([string]$Id) {
$cfg = Resolve-Platform $Id
Clone-Or-Update
Write-Host "→ Linking skills for $Id ($($cfg.Style) → $($cfg.Target))"
Link-Skills $cfg.Target $cfg.Style
Write-Host '→ Linking universal plugin root'
Link-Plugin-Root
Write-Host '→ Linking ugraph CLI'
Link-Cli

if ($Id -eq 'kiro') {
Write-Host '→ Creating Kiro agent configuration'
Expand Down Expand Up @@ -257,6 +289,7 @@ function Cmd-Uninstall([string]$Id) {
if (Remove-Reparse $PluginLink) {
Write-Host " ✓ removed $PluginLink"
}
Unlink-Cli
if (Test-Path $RepoDir) {
Write-Host "`nThe checkout at $RepoDir was kept (other platforms may still use it)."
Write-Host "To remove it: Remove-Item -Recurse -Force '$RepoDir'"
Expand Down
39 changes: 39 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
# Environment:
# UA_REPO_URL Override clone URL (default: official GitHub repo)
# UA_DIR Override clone destination (default: $HOME/.understand-anything/repo)
# UA_BIN_DIR Override CLI link directory (default: $HOME/.local/bin)

set -euo pipefail

REPO_URL="${UA_REPO_URL:-https://github.com/Egonex-AI/Understand-Anything.git}"
REPO_DIR="${UA_DIR:-$HOME/.understand-anything/repo}"
PLUGIN_LINK="$HOME/.understand-anything-plugin"
CLI_BIN_DIR="${UA_BIN_DIR:-$HOME/.local/bin}"
CLI_LINK="$CLI_BIN_DIR/ugraph"

# Platform table — id|skills-target-dir|style
# style "per-skill": one symlink per skill into the target dir
Expand Down Expand Up @@ -178,6 +181,38 @@ link_plugin_root() {
fi
}

link_cli() {
local target="$REPO_DIR/understand-anything-plugin/bin/ugraph.js"
if [[ ! -f "$target" ]]; then
printf ' • ugraph CLI not found at %s, skipping\n' "$target"
return 0
fi

mkdir -p "$CLI_BIN_DIR"
ln -sfn "$target" "$CLI_LINK"
printf ' ✓ %s → %s\n' "$CLI_LINK" "$target"

case ":$PATH:" in
*":$CLI_BIN_DIR:"*) ;;
*)
printf ' Tip: add %s to PATH to run `ugraph` from any shell.\n' "$CLI_BIN_DIR"
;;
esac
}

unlink_cli() {
if [[ ! -L "$CLI_LINK" ]]; then
return 0
fi

local resolved
resolved="$(readlink "$CLI_LINK" 2>/dev/null || true)"
if [[ "$resolved" == *"/understand-anything-plugin/bin/ugraph.js" ]]; then
rm -f "$CLI_LINK"
printf ' ✓ removed %s\n' "$CLI_LINK"
fi
}

cmd_install() {
local id="$1"
local row target style
Expand All @@ -190,6 +225,8 @@ cmd_install() {
link_skills "$target" "$style"
printf -- '→ Linking universal plugin root\n'
link_plugin_root
printf -- '→ Linking ugraph CLI\n'
link_cli

if [[ "$id" == "kiro" ]]; then
printf -- '→ Creating Kiro agent configuration\n'
Expand Down Expand Up @@ -248,6 +285,7 @@ cmd_uninstall() {
rm -f "$PLUGIN_LINK"
printf ' ✓ removed %s\n' "$PLUGIN_LINK"
fi
unlink_cli
if [[ -d "$REPO_DIR" ]]; then
printf '\nThe checkout at %s was kept (other platforms may still use it).\n' "$REPO_DIR"
printf 'To remove it: rm -rf "%s"\n' "$REPO_DIR"
Expand Down Expand Up @@ -279,6 +317,7 @@ $(platform_ids | sed 's/^/ - /')
Environment:
UA_REPO_URL Override clone URL (default: official repo)
UA_DIR Override clone destination (default: \$HOME/.understand-anything/repo)
UA_BIN_DIR Override CLI link directory (default: \$HOME/.local/bin)
USAGE
}

Expand Down
207 changes: 207 additions & 0 deletions tests/ugraph.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";

const repoRoot = path.resolve(import.meta.dirname, "..");
const cliPath = path.join(repoRoot, "understand-anything-plugin", "bin", "ugraph.js");

function withGraph(testFn) {
const dir = mkdtempSync(path.join(tmpdir(), "ugraph-"));
mkdirSync(path.join(dir, ".understand-anything"));
writeFileSync(
path.join(dir, ".understand-anything", "knowledge-graph.json"),
JSON.stringify(sampleGraph),
);

try {
return testFn(dir);
} finally {
rmSync(dir, { recursive: true, force: true });
}
}

function run(cwd, args) {
const output = execFileSync(process.execPath, [cliPath, ...args], {
cwd,
encoding: "utf8",
});
return JSON.parse(output);
}

const sampleGraph = {
version: "1.0.0",
kind: "codebase",
project: {
name: "Shop",
description: "Example commerce service",
languages: ["TypeScript"],
frameworks: ["Express"],
analyzedAt: "2026-06-15T00:00:00Z",
gitCommitHash: "abc123",
},
nodes: [
{
id: "file:src/auth/login.ts",
type: "file",
name: "login.ts",
filePath: "src/auth/login.ts",
summary: "Handles auth login and token validation.",
tags: ["auth", "security"],
complexity: "moderate",
},
{
id: "function:src/auth/login.ts:validateToken",
type: "function",
name: "validateToken",
filePath: "src/auth/login.ts",
lineRange: [10, 25],
summary: "Validates JWT tokens for authenticated requests.",
tags: ["auth", "jwt"],
complexity: "simple",
},
{
id: "file:src/routes/payment.ts",
type: "file",
name: "payment.ts",
filePath: "src/routes/payment.ts",
summary: "Payment route that requires authentication before checkout.",
tags: ["payment", "route"],
complexity: "moderate",
},
{
id: "file:src/db/pool.ts",
type: "file",
name: "pool.ts",
filePath: "src/db/pool.ts",
summary: "Database connection pool.",
tags: ["database"],
complexity: "simple",
},
{
id: "file:tests/auth.test.ts",
type: "file",
name: "auth.test.ts",
filePath: "tests/auth.test.ts",
summary: "Tests token validation and login behavior.",
tags: ["test", "auth"],
complexity: "simple",
},
],
edges: [
{
source: "file:src/auth/login.ts",
target: "function:src/auth/login.ts:validateToken",
type: "contains",
direction: "forward",
weight: 1,
},
{
source: "file:src/routes/payment.ts",
target: "function:src/auth/login.ts:validateToken",
type: "calls",
direction: "forward",
weight: 0.9,
},
{
source: "file:src/auth/login.ts",
target: "file:src/db/pool.ts",
type: "depends_on",
direction: "forward",
weight: 0.7,
},
{
source: "file:tests/auth.test.ts",
target: "file:src/auth/login.ts",
type: "tested_by",
direction: "backward",
weight: 0.8,
},
],
layers: [
{
id: "api",
name: "API",
description: "HTTP entry points",
nodeIds: ["file:src/routes/payment.ts"],
},
{
id: "auth",
name: "Auth",
description: "Authentication and authorization",
nodeIds: ["file:src/auth/login.ts", "function:src/auth/login.ts:validateToken"],
},
{
id: "data",
name: "Data",
description: "Persistence",
nodeIds: ["file:src/db/pool.ts"],
},
],
tour: [
{
order: 1,
title: "Start with auth",
description: "Understand login before reading payment routes.",
nodeIds: ["file:src/auth/login.ts", "file:src/routes/payment.ts"],
},
],
};

describe("ugraph CLI", () => {
it("prints a compact project overview", () => withGraph((cwd) => {
const result = run(cwd, ["overview"]);

expect(result.command).toBe("overview");
expect(result.project.name).toBe("Shop");
expect(result.graph.nodes).toBe(5);
expect(result.nodeTypes.file).toBe(4);
}));

it("finds relevant graph nodes", () => withGraph((cwd) => {
const result = run(cwd, ["find", "auth", "--limit", "3"]);

expect(result.command).toBe("find");
expect(result.results.length).toBeGreaterThan(0);
expect(result.results[0].node.tags).toContain("auth");
}));

it("shows a node with direct relationships", () => withGraph((cwd) => {
const result = run(cwd, ["node", "src/auth/login.ts"]);

expect(result.node.id).toBe("file:src/auth/login.ts");
expect(result.relationships.outgoing.map((edge) => edge.type)).toContain("contains");
expect(result.relationships.incoming.map((edge) => edge.source)).toContain("file:tests/auth.test.ts");
}));

it("resolves file paths by suffix for repo-relative callers", () => withGraph((cwd) => {
const result = run(cwd, ["node", "app/src/auth/login.ts", "--no-edges"]);

expect(result.node.id).toBe("file:src/auth/login.ts");
}));

it("expands neighborhoods by depth", () => withGraph((cwd) => {
const result = run(cwd, ["neighbors", "src/auth/login.ts", "--depth", "2", "--limit", "10"]);

const nodeIds = result.nodes.map((node) => node.id);
expect(nodeIds).toContain("file:src/db/pool.ts");
expect(nodeIds).toContain("file:src/routes/payment.ts");
}));

it("reports incoming impact for a changed file", () => withGraph((cwd) => {
const result = run(cwd, ["impact", "src/auth/login.ts", "--limit", "10"]);

const impactedIds = result.impactedNodes.map((node) => node.id);
expect(impactedIds).toContain("file:src/routes/payment.ts");
expect(impactedIds).toContain("file:tests/auth.test.ts");
}));

it("builds context around a query", () => withGraph((cwd) => {
const result = run(cwd, ["context", "payment checkout", "--limit", "10"]);

expect(result.command).toBe("context");
expect(result.matchedNodes.map((node) => node.id)).toContain("file:src/routes/payment.ts");
expect(result.relationships.length).toBeGreaterThan(0);
}));
});
Loading