Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
91 changes: 91 additions & 0 deletions .openclaw-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Understand-Anything — OpenClaw Gateway Plugin

A native [OpenClaw](https://github.com/openclaw/openclaw) gateway plugin that runs the
Understand-Anything knowledge-graph pipeline **inside the gateway process** — no
Claude Code, no skill/Task-tool dispatch, no per-platform symlinks.

This is a deeper integration than the `install.sh openclaw` skill symlink (which
still works and is unaffected): the gateway itself gains analysis tools that any
connected agent can call mid-conversation, plus a dashboard route.

## What it registers

**Agent tools** (available to every agent connected to the gateway):

| Tool | Purpose |
| --- | --- |
| `understand_list_projects` | List configured projects + analyzed status and graph size |
| `understand_analyze_project` | Start background analysis: tree-sitter structural pass + one LLM call per file, persisted to the project's `.ua/` dir |
| `understand_status` | Poll a running analysis job / inspect the persisted graph's metadata |
| `understand_search` | Fuzzy-search graph nodes (names, tags, summaries) via `@understand-anything/core`'s SearchEngine |
| `understand_get_node` | Full node detail + incoming/outgoing edges with neighbor names |

**HTTP routes** (mounted on the gateway):

- `GET /understand-anything` — project picker
- `GET /understand-anything/open?project=<idx>` — starts (or reuses) a
`understand-anything-viewer` instance for that project and redirects to its
token-protected dashboard URL. The viewer binds to 127.0.0.1 only.

## How it works

The pipeline (`src/pipeline.ts`) is built entirely on `@understand-anything/core`'s
public API: `createIgnoreFilter` + `LanguageRegistry` for the walk,
`TreeSitterPlugin.analyzeFile` for deterministic structure,
`buildFileAnalysisPrompt`/`parseFileAnalysisResponse` +
`buildProjectSummaryPrompt`/`parseProjectSummaryResponse` for LLM enrichment,
`GraphBuilder` → `validateGraph` → `saveGraph`/`saveMeta` for persistence. The
LLM step is a single-turn structured-JSON call per file against the Anthropic
Messages API (`src/llm.ts`) — bounded by `concurrency` and `maxFiles`.

Output is byte-compatible with the rest of the ecosystem: the same
`.ua/knowledge-graph.json` the skills produce, and the same dashboard renders.

## Install

From the repo root:

```bash
pnpm install
pnpm --filter @understand-anything/openclaw-plugin build
pnpm --filter understand-anything-viewer build # embeds the dashboard for the /understand-anything route
```

Then in your `~/.openclaw/openclaw.json`:

```jsonc
{
"plugins": {
"load": { "paths": ["/path/to/Understand-Anything/.openclaw-plugin"] },
"entries": {
"understand-anything": {
"enabled": true,
"config": {
"projects": ["/abs/path/to/project-a", "/abs/path/to/project-b"],
"model": "claude-sonnet-5", // optional
"concurrency": 5, // optional
"maxFiles": 400, // optional
"anthropicApiKey": "sk-ant-..." // optional; falls back to ANTHROPIC_API_KEY on the gateway process
}
}
}
}
}
```

Restart the gateway. Then, from any agent session:

```
understand_analyze_project { "project": "0" }
understand_status
understand_search { "query": "session management" }
```

## Security notes

- Analysis and serving are restricted to the configured `projects` allowlist.
- The dashboard viewer inherits upstream's security model: 127.0.0.1 bind,
per-instance random access token, graph-derived file allowlist, 1 MB/no-binary
caps on source preview.
- The API key is only read from plugin config or the gateway process env; it is
never written to disk by the plugin.
71 changes: 71 additions & 0 deletions .openclaw-plugin/openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"id": "understand-anything",
"name": "Understand Anything",
"version": "0.1.0",
"description": "Native OpenClaw gateway plugin for Understand-Anything: tree-sitter + LLM knowledge-graph pipeline running inside the gateway process, with a gateway-mounted dashboard and query tools — no Claude Code / Task-tool dispatch required.",
"author": "Egonex (https://github.com/Egonex-AI/Understand-Anything), OpenClaw port by Tank",
"main": "dist/index.js",
"activation": {
"onStartup": true
},
"contracts": {
"tools": [
"understand_list_projects",
"understand_analyze_project",
"understand_status",
"understand_search",
"understand_get_node"
]
},
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"anthropicApiKey": {
"type": "string",
"description": "Anthropic API key for the LLM enrichment phase. Falls back to the gateway's default anthropic:default auth profile when unset."
},
"model": {
"type": "string",
"description": "Anthropic model id used for file/project/layer/tour analysis calls.",
"default": "claude-sonnet-5"
},
"concurrency": {
"type": "number",
"description": "Max concurrent per-file LLM analysis calls during a project scan.",
"default": 5,
"minimum": 1,
"maximum": 20
},
"maxFiles": {
"type": "number",
"description": "Safety cap on how many recognized source files a single analysis run will send to the LLM (cost/runtime bound on large repos).",
"default": 400,
"minimum": 1
},
"projects": {
"type": "array",
"items": { "type": "string" },
"description": "Absolute paths to projects this plugin is allowed to analyze/serve. Analysis and dashboard routes only operate on directories listed here; when empty or unset the plugin activates but every tool reports an empty project list."
}
}
},
"uiHints": {
"anthropicApiKey": {
"label": "Anthropic API Key",
"secret": true
},
"model": {
"label": "Analysis Model"
},
"concurrency": {
"label": "Analysis Concurrency"
},
"maxFiles": {
"label": "Max Files Per Analysis Run"
},
"projects": {
"label": "Allowed Project Paths"
}
}
}
34 changes: 34 additions & 0 deletions .openclaw-plugin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@understand-anything/openclaw-plugin",
"version": "0.1.0",
"type": "module",
"private": true,
"description": "OpenClaw gateway plugin: native tree-sitter + LLM knowledge-graph pipeline and dashboard for Understand-Anything.",
"main": "dist/index.js",
"openclaw": {
"extensions": [
"./dist/index.js"
]
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"test": "vitest run"
},
"dependencies": {
"@sinclair/typebox": "^0.34.0",
"@understand-anything/core": "workspace:*",
"understand-anything-viewer": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.2.1",
"typescript": "^5.7.0",
"vitest": "^3.1.0"
},
"files": [
"dist",
"openclaw.plugin.json",
"README.md"
]
}
36 changes: 36 additions & 0 deletions .openclaw-plugin/src/__tests__/glob-match.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { describe, expect, it } from "vitest";
import { globToRegExp, matchesAnyPattern } from "../glob-match.js";

describe("globToRegExp", () => {
it("matches ** across path segments", () => {
expect(globToRegExp("src/api/**").test("src/api/v1/users.ts")).toBe(true);
expect(globToRegExp("src/api/**").test("src/web/users.ts")).toBe(false);
});

it("keeps * within a single segment", () => {
expect(globToRegExp("src/*.ts").test("src/main.ts")).toBe(true);
expect(globToRegExp("src/*.ts").test("src/nested/main.ts")).toBe(false);
});

it("matches ? as exactly one non-separator character", () => {
expect(globToRegExp("file?.ts").test("file1.ts")).toBe(true);
expect(globToRegExp("file?.ts").test("file12.ts")).toBe(false);
expect(globToRegExp("file?.ts").test("file/.ts")).toBe(false);
});

it("escapes regex metacharacters in literal parts", () => {
expect(globToRegExp("src/a+b.ts").test("src/a+b.ts")).toBe(true);
expect(globToRegExp("src/a+b.ts").test("src/aab.ts")).toBe(false);
expect(globToRegExp("*.test.ts").test("foo.test.ts")).toBe(true);
expect(globToRegExp("*.test.ts").test("foo.testxts")).toBe(false);
});
});

describe("matchesAnyPattern", () => {
it("returns true when any pattern matches", () => {
expect(matchesAnyPattern("src/api/users.ts", ["docs/**", "src/api/**"])).toBe(true);
});
it("returns false when no pattern matches", () => {
expect(matchesAnyPattern("src/web/index.ts", ["docs/**", "src/api/**"])).toBe(false);
});
});
43 changes: 43 additions & 0 deletions .openclaw-plugin/src/__tests__/resolve-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expect, it } from "vitest";
import { resolveImportTarget } from "../pipeline.js";

describe("resolveImportTarget", () => {
const known = new Set([
"src/main.ts",
"src/util.ts",
"src/components/Button.tsx",
"src/lib/index.ts",
"scripts/run.mjs",
"pkg/mod.py",
]);

it("resolves a TS-ESM .js specifier to the .ts file on disk (regression: util.js.ts)", () => {
expect(resolveImportTarget("src/main.ts", "./util.js", known)).toBe("src/util.ts");
});

it("resolves an extensionless specifier by trying known extensions", () => {
expect(resolveImportTarget("src/main.ts", "./util", known)).toBe("src/util.ts");
expect(resolveImportTarget("src/main.ts", "./components/Button", known)).toBe("src/components/Button.tsx");
});

it("resolves a directory import to its index file", () => {
expect(resolveImportTarget("src/main.ts", "./lib", known)).toBe("src/lib/index.ts");
});

it("resolves an exact-match specifier as written", () => {
expect(resolveImportTarget("scripts/other.mjs", "./run.mjs", known)).toBe("scripts/run.mjs");
});

it("resolves ../ traversal", () => {
expect(resolveImportTarget("src/components/Button.tsx", "../util.js", known)).toBe("src/util.ts");
});

it("skips bare package imports", () => {
expect(resolveImportTarget("src/main.ts", "react", known)).toBeNull();
expect(resolveImportTarget("src/main.ts", "@scope/pkg", known)).toBeNull();
});

it("returns null for unresolvable relative imports", () => {
expect(resolveImportTarget("src/main.ts", "./missing.js", known)).toBeNull();
});
});
Loading