diff --git a/.gitignore b/.gitignore index 12ebe52d1..043093322 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ node_modules dist .understand-anything +.ua *.tsbuildinfo .DS_Store .env diff --git a/README.md b/README.md index 04efe8311..0970b202b 100644 --- a/README.md +++ b/README.md @@ -140,10 +140,28 @@ A multi-agent pipeline scans your project, extracts every file, function, class, On the **first run** in a project — when you don't pass `--language` and no language is stored yet — `/understand` detects the language you're conversing in. If it isn't English, it asks you to confirm (or override) before generating; English conversations are unaffected. Your choice is saved to `.ua/config.json` and reused on every later run. +**Project-level tree-sitter extension aliases:** If your codebase uses custom file extensions, add them to `.ua/config.json` (or the legacy `.understand-anything/config.json` if your project already uses that directory): + +```json +{ + "treeSitter": { + "extensionLanguageMap": { + ".customts": "typescript", + ".custompy": "python" + } + } +} +``` + +This lets `/understand` treat those extensions as the specified language during tree-sitter-based structural scanning. + The `--language` parameter affects: - Node summaries and descriptions in the knowledge graph - Dashboard UI labels, buttons, and tooltips - Guided tour explanations +- `/understand-domain` output (domain, flow, step names/summaries) +- `/understand-onboard` generated onboarding guide text +- `/understand-knowledge` inferred entity/claim/edge descriptions ### 3. Explore the dashboard @@ -171,6 +189,15 @@ An interactive web dashboard opens with your codebase visualized as a graph — # Extract business domain knowledge (domains, flows, steps) /understand-domain +# Extract domain knowledge in Chinese (also persisted to config.json) +/understand-domain --language zh + +# Generate onboarding guide in Chinese +/understand-onboard --language zh + +# Analyze Karpathy wiki and output inferred knowledge in Chinese +/understand-knowledge ~/path/to/wiki --language zh + # Analyze a Karpathy-pattern LLM wiki knowledge base /understand-knowledge ~/path/to/wiki @@ -213,7 +240,7 @@ curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/ iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/install.ps1 | iex ``` -The installer clones the repo to `~/.understand-anything/repo` and creates the right symlinks for the chosen platform. Restart your CLI/IDE afterwards. +The installer clones the repo to `~/.understand-anything/repo` and creates the right symlinks for the chosen platform. For platforms with a supported agent-profile directory, it also links the bundled `agents/*.md` profiles so the multi-agent pipeline can run without deterministic fallback. Restart your CLI/IDE afterwards. > **Note on invoking skills:** the invocation prefix differs per platform. Most platforms use slash commands (`/understand`), but **Codex uses `$` instead** — type `$understand`, not `/understand`. If neither prefix is recognized on your platform, just ask in plain language: *"Use the understand skill to analyze this project."* diff --git a/READMEs/README.es-ES.md b/READMEs/README.es-ES.md index 69a6e2d65..ab771deb8 100644 --- a/READMEs/README.es-ES.md +++ b/READMEs/README.es-ES.md @@ -5,6 +5,12 @@ Compatible con Claude Code, Codex, Cursor, Copilot, Gemini CLI y más.

+

+ Understand Anything. Understand Anyone. +
+ La IA debería ayudar a las personas, no reemplazarlas. +

+

Understand Anything | Trendshift

@@ -22,8 +28,11 @@ Copilot CLI Gemini CLI OpenCode + Vibe CLI + Trae Homepage Live Demo + Understand Anyone

@@ -128,10 +137,30 @@ Un pipeline multi-agente escanea tu proyecto, extrae cada archivo, función, cla # Idiomas soportados: en (default), zh, zh-TW, ja, ko, ru ``` +En la **primera ejecución** en un proyecto — cuando no pasas `--language` y no hay un idioma guardado aún — `/understand` detecta el idioma en el que estás conversando. Si no es inglés, te pide confirmar (o cambiar) antes de generar; las conversaciones en inglés no se ven afectadas. Tu elección se guarda en `.ua/config.json` y se reutiliza en cada ejecución posterior. + +**Alias de extensiones tree-sitter a nivel de proyecto:** Si tu código usa extensiones de archivo personalizadas, puedes agregarlas a `.ua/config.json` (o al legacy `.understand-anything/config.json` si tu proyecto ya usa ese directorio): + +```json +{ + "treeSitter": { + "extensionLanguageMap": { + ".customts": "typescript", + ".custompy": "python" + } + } +} +``` + +Esto permite que `/understand` trate esas extensiones como el lenguaje especificado durante el escaneo estructural basado en tree-sitter. + El parámetro `--language` afecta: - Resúmenes y descripciones de nodos en el grafo de conocimiento - Etiquetas, botones y tooltips de la UI del dashboard - Explicaciones de los tours guiados +- Salida de `/understand-domain` (nombres/resúmenes de dominios, flujos, pasos) +- Texto de la guía de incorporación generada por `/understand-onboard` +- Descripciones de entidades/afirmaciones/relaciones inferidas por `/understand-knowledge` ### 3. Explora el panel diff --git a/READMEs/README.ja-JP.md b/READMEs/README.ja-JP.md index 0cb341654..c3e4faa59 100644 --- a/READMEs/README.ja-JP.md +++ b/READMEs/README.ja-JP.md @@ -140,10 +140,28 @@ Understand Anything は [Claude Code Plugin](https://code.claude.com/docs/en/plu プロジェクトでの**初回実行時**に `--language` を指定せず、保存済みの言語設定もない場合、`/understand` は会話で使われている言語を検出します。英語以外が検出された場合は、生成前にその言語を使用するか、別の言語へ変更するかを確認します。英語での会話には影響しません。選択結果は `.ua/config.json` に保存され、以降の実行でも再利用されます。 +**プロジェクトレベルの tree-sitter 拡張子エイリアス:** コードベースでカスタムファイル拡張子を使用している場合、`.ua/config.json` に追加できます(またはプロジェクトが既存の `.understand-anything/` ディレクトリを使用している場合はそのディレクトリの `config.json`): + +```json +{ + "treeSitter": { + "extensionLanguageMap": { + ".customts": "typescript", + ".custompy": "python" + } + } +} +``` + +これにより、`/understand` は tree-sitter ベースの構造スキャンでそれらの拡張子を指定された言語として扱います。 + `--language` パラメータは以下に影響します: - ナレッジグラフのノードサマリーと説明 - ダッシュボードUIのラベル、ボタン、ツールチップ - ガイド付きツアーの説明 +- `/understand-domain` の出力(ドメイン、フロー、ステップの名前/概要) +- `/understand-onboard` で生成されるオンボーディングガイドのテキスト +- `/understand-knowledge` で推論されるエンティティ/主張/関係の説明 ### 3. ダッシュボードで探索 diff --git a/READMEs/README.ko-KR.md b/READMEs/README.ko-KR.md index 1d0185e97..52d0b226b 100644 --- a/READMEs/README.ko-KR.md +++ b/READMEs/README.ko-KR.md @@ -5,6 +5,12 @@ Claude Code, Codex, Cursor, Copilot, Gemini CLI 등 다양한 플랫폼을 지원합니다.

+

+ Understand Anything. Understand Anyone. +
+ AI는 사람을 대체하는 것이 아니라, 사람을 도와야 합니다. +

+

Understand Anything | Trendshift

@@ -22,8 +28,11 @@ Copilot CLI Gemini CLI OpenCode + Vibe CLI + Trae Homepage Live Demo + Understand Anyone

@@ -128,10 +137,30 @@ Understand Anything은 [Claude Code Plugin](https://code.claude.com/docs/en/plug # 지원 언어: en(기본값), zh, zh-TW, ja, ko, ru ``` +프로젝트에서의 **첫 실행 시** — `--language`를 지정하지 않고 저장된 언어 설정도 없을 때 — `/understand`는 대화에서 사용 중인 언어를 감지합니다. 영어가 아닌 것이 감지되면, 생성 전에 해당 언어를 사용할지 다른 언어로 변경할지 확인합니다. 영어 대화에는 영향이 없습니다. 선택 결과는 `.ua/config.json`에 저장되어 이후 실행에서도 재사용됩니다. + +**프로젝트 수준 tree-sitter 확장 별칭:** 코드베이스에서 커스텀 파일 확장자를 사용하는 경우, `.ua/config.json`에 추가할 수 있습니다 (또는 프로젝트가 기존 `.understand-anything/` 디렉토리를 사용 중이라면 해당 디렉토리의 `config.json`): + +```json +{ + "treeSitter": { + "extensionLanguageMap": { + ".customts": "typescript", + ".custompy": "python" + } + } +} +``` + +이렇게 하면 `/understand`가 tree-sitter 기반 구조 스캔에서 해당 확장자를 지정된 언어로 처리합니다. + `--language` 매개변수는 다음에 영향합니다: - 지식 그래프의 노드 요약과 설명 - 대시보드 UI의 레이블, 버튼, 툴팁 - 가이드 투어의 설명 +- `/understand-domain` 출력 (도메인, 흐름, 단계의 이름/요약) +- `/understand-onboard` 생성된 온보딩 가이드 텍스트 +- `/understand-knowledge` 추론된 엔티티/주장/관계 설명 ### 3. 대시보드 탐색 @@ -145,7 +174,7 @@ Understand Anything은 [Claude Code Plugin](https://code.claude.com/docs/en/plug ```bash # 코드베이스에 대해 무엇이든 질문하기 -/understand-chat How does the payment flow work? +/understand-chat 결제 흐름은 어떻게 작동하나요? # 현재 변경 사항의 영향 분석 /understand-diff @@ -159,6 +188,15 @@ Understand Anything은 [Claude Code Plugin](https://code.claude.com/docs/en/plug # 비즈니스 도메인 지식 추출 (도메인, 흐름, 단계) /understand-domain +# 한국어로 도메인 지식 추출 (config.json에도 저장됨) +/understand-domain --language ko + +# 한국어로 온보딩 가이드 생성 +/understand-onboard --language ko + +# 한국어로 Karpathy wiki 분석 및 추론된 지식 출력 +/understand-knowledge ~/path/to/wiki --language ko + # Karpathy 패턴 LLM 위키 지식 베이스 분석 /understand-knowledge ~/path/to/wiki @@ -185,7 +223,7 @@ Understand-Anything은 다양한 AI 코딩 플랫폼에서 사용할 수 있습 /plugin install understand-anything ``` -### 한 줄 설치 (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot / Kiro) +### 한 줄 설치 (Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Trae / Nanobot / Kiro) **macOS / Linux:** ```bash @@ -203,7 +241,7 @@ iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/i > **스킬 호출 방식 안내:** 호출 접두사는 플랫폼마다 다릅니다. 대부분의 플랫폼은 슬래시 명령(`/understand`)을 사용하지만, **Codex는 `$`를 사용합니다** — `/understand`가 아니라 `$understand`를 입력하세요. 두 접두사 모두 인식되지 않으면 *"understand 스킬로 이 프로젝트를 분석해 줘"* 처럼 자연어로 요청하면 됩니다. -- 지원되는 `` 값: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `nanobot`, `kiro` +- 지원되는 `` 값: `gemini`, `codex`, `opencode`, `pi`, `openclaw`, `antigravity`, `vibe`, `vscode`, `hermes`, `cline`, `kimi`, `trae`, `nanobot`, `kiro` - 이후 업데이트: `./install.sh --update` - 제거: `./install.sh --uninstall ` @@ -255,6 +293,7 @@ curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/ | Hermes | ✅ 지원 | `install.sh hermes` | | Cline | ✅ 지원 | `install.sh cline` | | KIMI CLI | ✅ 지원 | `install.sh kimi` | +| Trae | ✅ 지원 | `install.sh trae` | | Nanobot | ✅ 지원 | `install.sh nanobot` | | Kiro CLI / IDE | ✅ 지원 | `install.sh kiro` | diff --git a/READMEs/README.zh-CN.md b/READMEs/README.zh-CN.md index d3ba928fe..4aa2473de 100644 --- a/READMEs/README.zh-CN.md +++ b/READMEs/README.zh-CN.md @@ -5,6 +5,12 @@ 支持 Claude Code、Codex、Cursor、Copilot、Gemini CLI 等多平台。

+

+ Understand Anything. Understand Anyone. +
+ AI 应该帮助人,而不是取代人。 +

+

Understand Anything | Trendshift

@@ -22,8 +28,11 @@ Copilot CLI Gemini CLI OpenCode + Vibe CLI + Trae Homepage Live Demo + Understand Anyone

@@ -128,10 +137,30 @@ Understand Anything 是一个 [Claude Code Plugin](https://code.claude.com/docs/ # 支持的语言:en(默认)、zh、zh-TW、ja、ko、ru ``` +在项目的**首次运行**时 —— 当你没有传入 `--language` 且尚未存储语言设置时 —— `/understand` 会检测你正在使用的语言。如果检测到非英语,它会询问你确认(或改用其他语言)后再生成;英语对话不受影响。你的选择会保存到 `.ua/config.json` 并在后续运行中复用。 + +**项目级 tree-sitter 扩展别名:** 如果你的代码库使用自定义文件扩展名,可以在 `.ua/config.json` 中添加(如果你的项目仍在使用旧版 `.understand-anything/` 目录,也可以使用该目录下的 `config.json`): + +```json +{ + "treeSitter": { + "extensionLanguageMap": { + ".customts": "typescript", + ".custompy": "python" + } + } +} +``` + +这样 `/understand` 在基于 tree-sitter 的结构扫描时就会将这些扩展名视为指定语言。 + `--language` 参数会影响: - 知识图谱中的节点摘要和描述 - Dashboard UI 的标签、按钮和提示 - 导览路线的解释说明 +- `/understand-domain` 输出(领域、流程、步骤的名称/摘要) +- `/understand-onboard` 生成的入门指南文本 +- `/understand-knowledge` 推断的实体/论断/关系描述 ### 3. 打开数据看板 @@ -159,6 +188,15 @@ Understand Anything 是一个 [Claude Code Plugin](https://code.claude.com/docs/ # 提取业务领域知识(领域、流程、步骤) /understand-domain +# 以中文提取领域知识(也会保存到 config.json) +/understand-domain --language zh + +# 以中文生成入门指南 +/understand-onboard --language zh + +# 以中文分析 Karpathy wiki 并输出推断的知识 +/understand-knowledge ~/path/to/wiki --language zh + # 分析 Karpathy 模式的 LLM Wiki 知识库 /understand-knowledge ~/path/to/wiki @@ -185,7 +223,7 @@ Understand-Anything 可在多个 AI 编码平台上运行。 /plugin install understand-anything ``` -### 一行命令安装(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot / Kiro) +### 一行命令安装(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Trae / Nanobot / Kiro) **macOS / Linux:** ```bash @@ -203,7 +241,7 @@ iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/i > **关于技能调用方式:** 不同平台的调用前缀不同。大多数平台使用斜杠命令(`/understand`),但 **Codex 使用 `$`** —— 请输入 `$understand`,而不是 `/understand`。如果两种前缀都不被识别,直接用自然语言请求即可:*“使用 understand 技能分析这个项目”*。 -- 支持的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`nanobot`、`kiro` +- 支持的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`trae`、`nanobot`、`kiro` - 后续更新:`./install.sh --update` - 卸载:`./install.sh --uninstall ` @@ -255,6 +293,7 @@ curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/ | Hermes | ✅ 支持 | `install.sh hermes` | | Cline | ✅ 支持 | `install.sh cline` | | KIMI CLI | ✅ 支持 | `install.sh kimi` | +| Trae | ✅ 支持 | `install.sh trae` | | Nanobot | ✅ 支持 | `install.sh nanobot` | | Kiro CLI / IDE | ✅ 支持 | `install.sh kiro` | diff --git a/READMEs/README.zh-TW.md b/READMEs/README.zh-TW.md index f7b145fc1..bb0121f1e 100644 --- a/READMEs/README.zh-TW.md +++ b/READMEs/README.zh-TW.md @@ -5,6 +5,12 @@ 支援 Claude Code、Codex、Cursor、Copilot、Gemini CLI 等多平台。

+

+ Understand Anything. Understand Anyone. +
+ AI 應該幫助人,而不是取代人。 +

+

Understand Anything | Trendshift

@@ -22,8 +28,11 @@ Copilot CLI Gemini CLI OpenCode + Vibe CLI + Trae Homepage Live Demo + Understand Anyone

@@ -128,10 +137,30 @@ Understand Anything 是一個 [Claude Code Plugin](https://code.claude.com/docs/ # 支援的語言:en(預設)、zh、zh-TW、ja、ko、ru ``` +在專案的**首次執行**時 —— 當你沒有傳入 `--language` 且尚未儲存語言設定時 —— `/understand` 會檢測你正在使用的語言。如果檢測到非英語,它會詢問你確認(或改用其他語言)後再生成;英語對話不受影響。你的選擇會儲存到 `.ua/config.json` 並在後續執行中復用。 + +**專案級 tree-sitter 擴充別名:** 如果你的程式碼庫使用自訂檔案副檔名,可以在 `.ua/config.json` 中新增(或若你的專案仍在使用舊版 `.understand-anything/` 目錄,也可使用該目錄下的 `config.json`): + +```json +{ + "treeSitter": { + "extensionLanguageMap": { + ".customts": "typescript", + ".custompy": "python" + } + } +} +``` + +這樣 `/understand` 在基於 tree-sitter 的結構掃描時就會將這些副檔名視為指定語言。 + `--language` 參數會影響: - 知識圖譜中的節點摘要和描述 - Dashboard UI 的標籤、按鈕和提示 --導覽路線的解釋說明 +- 導覽路線的解釋說明 +- `/understand-domain` 輸出(領域、流程、步驟的名稱/摘要) +- `/understand-onboard` 生成的入門指南文字 +- `/understand-knowledge` 推斷的實體/論斷/關係描述 ### 3. 開啟資料看板 @@ -159,6 +188,15 @@ Understand Anything 是一個 [Claude Code Plugin](https://code.claude.com/docs/ # 提取業務領域知識(領域、流程、步驟) /understand-domain +# 以繁體中文提取領域知識(也會儲存到 config.json) +/understand-domain --language zh-TW + +# 以繁體中文生成入門指南 +/understand-onboard --language zh-TW + +# 以繁體中文分析 Karpathy wiki 並輸出推斷的知識 +/understand-knowledge ~/path/to/wiki --language zh-TW + # 分析 Karpathy 模式的 LLM Wiki 知識庫 /understand-knowledge ~/path/to/wiki @@ -185,7 +223,7 @@ Understand-Anything 可在多個 AI 編碼平台上執行。 /plugin install understand-anything ``` -### 一行指令安裝(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Nanobot / Kiro) +### 一行指令安裝(Codex / OpenCode / OpenClaw / Antigravity / Gemini CLI / Pi Agent / Vibe CLI / VS Code Copilot / Hermes / Cline / KIMI CLI / Trae / Nanobot / Kiro) **macOS / Linux:** ```bash @@ -203,7 +241,7 @@ iwr -useb https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/i > **關於技能呼叫方式:** 不同平台的呼叫前綴不同。大多數平台使用斜線指令(`/understand`),但 **Codex 使用 `$`** —— 請輸入 `$understand`,而不是 `/understand`。如果兩種前綴都無法辨識,直接用自然語言請求即可:*「使用 understand 技能分析這個專案」*。 -- 支援的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`nanobot`、`kiro` +- 支援的 `` 取值:`gemini`、`codex`、`opencode`、`pi`、`openclaw`、`antigravity`、`vibe`、`vscode`、`hermes`、`cline`、`kimi`、`trae`、`nanobot`、`kiro` - 後續更新:`./install.sh --update` - 解除安裝:`./install.sh --uninstall ` @@ -255,6 +293,7 @@ curl -fsSL https://raw.githubusercontent.com/Egonex-AI/Understand-Anything/main/ | Hermes | ✅ 支援 | `install.sh hermes` | | Cline | ✅ 支援 | `install.sh cline` | | KIMI CLI | ✅ 支援 | `install.sh kimi` | +| Trae | ✅ 支援 | `install.sh trae` | | Nanobot | ✅ 支援 | `install.sh nanobot` | | Kiro CLI / IDE | ✅ 支援 | `install.sh kiro` | diff --git a/install.ps1 b/install.ps1 index 77c6595e4..96b4bd338 100644 --- a/install.ps1 +++ b/install.ps1 @@ -26,22 +26,23 @@ $RepoUrl = if ($env:UA_REPO_URL) { $env:UA_REPO_URL } else { 'https://github. $RepoDir = if ($env:UA_DIR) { $env:UA_DIR } else { Join-Path $HOME '.understand-anything\repo' } $PluginLink = Join-Path $HOME '.understand-anything-plugin' -# Platform table — Target = skills directory; Style = "per-skill" | "folder" +# Platform table — Target = skills directory; Style = "per-skill" | "folder"; +# AgentsTarget is empty for platforms that need a custom agent bundle format. $Platforms = [ordered]@{ - gemini = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill' } - codex = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill' } - opencode = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill' } - pi = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill' } - openclaw = @{ Target = (Join-Path $HOME '.openclaw\skills'); Style = 'folder' } - antigravity = @{ Target = (Join-Path $HOME '.gemini\antigravity\skills'); Style = 'folder' } - vibe = @{ Target = (Join-Path $HOME '.vibe\skills'); Style = 'per-skill' } - vscode = @{ Target = (Join-Path $HOME '.copilot\skills'); Style = 'per-skill' } - hermes = @{ Target = (Join-Path $HOME '.hermes\skills'); Style = 'folder' } - cline = @{ Target = (Join-Path $HOME '.cline\skills'); Style = 'folder' } - kimi = @{ Target = (Join-Path $HOME '.kimi\skills'); Style = 'folder' } - trae = @{ Target = (Join-Path $HOME '.trae\skills'); Style = 'per-skill' } - nanobot = @{ Target = (Join-Path $HOME '.nanobot\workspace\skills'); Style = 'per-skill' } - kiro = @{ Target = (Join-Path $HOME '.kiro\skills'); Style = 'per-skill' } + gemini = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.agents\agents') } + codex = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.agents\agents') } + opencode = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.agents\agents') } + pi = @{ Target = (Join-Path $HOME '.agents\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.agents\agents') } + openclaw = @{ Target = (Join-Path $HOME '.openclaw\skills'); Style = 'folder'; AgentsTarget = (Join-Path $HOME '.openclaw\agents') } + antigravity = @{ Target = (Join-Path $HOME '.gemini\antigravity\skills'); Style = 'folder'; AgentsTarget = (Join-Path $HOME '.gemini\antigravity\agents') } + vibe = @{ Target = (Join-Path $HOME '.vibe\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.vibe\agents') } + vscode = @{ Target = (Join-Path $HOME '.copilot\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.copilot\agents') } + hermes = @{ Target = (Join-Path $HOME '.hermes\skills'); Style = 'folder'; AgentsTarget = (Join-Path $HOME '.hermes\agents') } + cline = @{ Target = (Join-Path $HOME '.cline\skills'); Style = 'folder'; AgentsTarget = (Join-Path $HOME '.cline\agents') } + kimi = @{ Target = (Join-Path $HOME '.kimi\skills'); Style = 'folder'; AgentsTarget = (Join-Path $HOME '.kimi\agents') } + trae = @{ Target = (Join-Path $HOME '.trae\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.trae\agents') } + nanobot = @{ Target = (Join-Path $HOME '.nanobot\workspace\skills'); Style = 'per-skill'; AgentsTarget = (Join-Path $HOME '.nanobot\workspace\agents') } + kiro = @{ Target = (Join-Path $HOME '.kiro\skills'); Style = 'per-skill'; AgentsTarget = '' } } function Show-Usage { @@ -85,6 +86,7 @@ function Prompt-Platform { } function Get-SkillsRoot { Join-Path $RepoDir 'understand-anything-plugin\skills' } +function Get-AgentsRoot { Join-Path $RepoDir 'understand-anything-plugin\agents' } function Clone-Or-Update { if (Test-Path (Join-Path $RepoDir '.git')) { @@ -194,6 +196,47 @@ function Link-Plugin-Root { } } +function Link-AgentProfiles([string]$Target) { + if (-not $Target) { return } + $root = Get-AgentsRoot + if (-not (Test-Path $root)) { Write-Error "Agents directory not found: $root" } + if (-not (Test-Path $Target)) { New-Item -ItemType Directory -Path $Target | Out-Null } + + Get-ChildItem -Path $root -Filter '*.md' -File | Sort-Object Name | ForEach-Object { + $link = Join-Path $Target $_.Name + # Agent profiles are Markdown FILES — New-Junction creates NTFS + # directory junctions which only target directories. Use file + # symbolic links instead. + if (Test-IsReparse $link) { + (Get-Item -LiteralPath $link -Force).Delete() + } elseif (Test-Path $link) { + Write-Error "Refusing to overwrite $link — it is a real file, not a symlink. Move or remove it first." + } + New-Item -ItemType SymbolicLink -Path $link -Target $_.FullName | Out-Null + Write-Host " ✓ $link → $($_.FullName)" + } +} + +function Unlink-AgentProfiles([string]$Target) { + if (-not $Target) { return } + if (-not (Test-Path $Target)) { return } + + $root = Get-AgentsRoot + if (Test-Path $root) { + Get-ChildItem -Path $root -Filter '*.md' -File | ForEach-Object { + Remove-Reparse (Join-Path $Target $_.Name) | Out-Null + } + } else { + Get-ChildItem -LiteralPath $Target -Filter '*.md' -Force | ForEach-Object { + if ($_.LinkType -eq 'Junction' -or $_.LinkType -eq 'SymbolicLink') { + if ($_.Target -match 'understand-anything-plugin[\\/]+agents[\\/]+') { + Remove-Reparse $_.FullName | Out-Null + } + } + } + } +} + function ConvertTo-FileUri([string]$Path) { # Produce a forward-slashed file URI (Windows: file:///C:/path/...). return 'file:///' + ($Path -replace '\\', '/') @@ -207,6 +250,11 @@ function Cmd-Install([string]$Id) { Write-Host '→ Linking universal plugin root' Link-Plugin-Root + if ($cfg.AgentsTarget) { + Write-Host "→ Linking agent profiles ($($cfg.AgentsTarget))" + Link-AgentProfiles $cfg.AgentsTarget + } + if ($Id -eq 'kiro') { Write-Host '→ Creating Kiro agent configuration' $agentsDir = Join-Path $HOME '.kiro\agents' @@ -258,6 +306,10 @@ function Cmd-Uninstall([string]$Id) { Write-Host " ✓ removed $agentJson" } } + if ($cfg.AgentsTarget) { + Write-Host '→ Removing agent profile links' + Unlink-AgentProfiles $cfg.AgentsTarget + } if (Remove-Reparse $PluginLink) { Write-Host " ✓ removed $PluginLink" } diff --git a/install.sh b/install.sh index eb7336f46..2bdcdd72e 100755 --- a/install.sh +++ b/install.sh @@ -22,26 +22,27 @@ 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" -# Platform table — id|skills-target-dir|style +# Platform table — id|skills-target-dir|style|agents-target-dir # style "per-skill": one symlink per skill into the target dir # style "folder": one symlink for the whole skills/ dir into the target, # named "understand-anything" +# agents-target-dir is empty for platforms that need a custom agent bundle format. platforms_table() { cat <&2 + exit 1 + fi + mkdir -p "$target" + while IFS= read -r agent_md; do + [[ -n "$agent_md" ]] || continue + ln -sfn "$agent_md" "$target/$(basename "$agent_md")" + printf ' ✓ %s → %s\n' "$target/$(basename "$agent_md")" "$agent_md" + done < <(find "$root" -maxdepth 1 -type f -name '*.md' | LC_ALL=C sort) +} + +unlink_agent_profiles() { + local target="$1" root agent_md link resolved + [[ -n "$target" ]] || return 0 + root="$(agents_root)" + [[ -d "$target" ]] || return 0 + if [[ -d "$root" ]]; then + while IFS= read -r agent_md; do + [[ -n "$agent_md" ]] || continue + link="$target/$(basename "$agent_md")" + [[ -L "$link" ]] && rm -f "$link" + done < <(find "$root" -maxdepth 1 -type f -name '*.md' | LC_ALL=C sort) + else + for link in "$target"/*.md; do + [[ -L "$link" ]] || continue + resolved="$(readlink "$link" 2>/dev/null || true)" + [[ "$resolved" == *"/understand-anything-plugin/agents/"* ]] || continue + rm -f "$link" + done + fi +} + link_plugin_root() { if [[ -L "$PLUGIN_LINK" || -e "$PLUGIN_LINK" ]]; then printf ' • %s already exists, leaving as-is\n' "$PLUGIN_LINK" @@ -180,10 +219,11 @@ link_plugin_root() { cmd_install() { local id="$1" - local row target style + local row target style agents_target row="$(resolve_platform "$id")" target="$(printf '%s\n' "$row" | cut -d'|' -f2)" style="$(printf '%s\n' "$row" | cut -d'|' -f3)" + agents_target="$(printf '%s\n' "$row" | cut -d'|' -f4)" clone_or_update printf -- '→ Linking skills for %s (%s → %s)\n' "$id" "$style" "$target" @@ -191,6 +231,11 @@ cmd_install() { printf -- '→ Linking universal plugin root\n' link_plugin_root + if [[ -n "$agents_target" ]]; then + printf -- '→ Linking agent profiles (%s)\n' "$agents_target" + link_agent_profiles "$agents_target" + fi + if [[ "$id" == "kiro" ]]; then printf -- '→ Creating Kiro agent configuration\n' mkdir -p "$HOME/.kiro/agents" @@ -236,10 +281,11 @@ KIROEOF cmd_uninstall() { local id="$1" - local row target style + local row target style agents_target row="$(resolve_platform "$id")" target="$(printf '%s\n' "$row" | cut -d'|' -f2)" style="$(printf '%s\n' "$row" | cut -d'|' -f3)" + agents_target="$(printf '%s\n' "$row" | cut -d'|' -f4)" printf -- '→ Removing skill links for %s\n' "$id" unlink_skills "$target" "$style" @@ -247,6 +293,10 @@ cmd_uninstall() { rm -f "$HOME/.kiro/agents/understand.json" printf ' ✓ removed %s\n' "$HOME/.kiro/agents/understand.json" fi + if [[ -n "$agents_target" ]]; then + printf -- '→ Removing agent profile links\n' + unlink_agent_profiles "$agents_target" + fi if [[ -L "$PLUGIN_LINK" ]]; then rm -f "$PLUGIN_LINK" printf ' ✓ removed %s\n' "$PLUGIN_LINK" diff --git a/tests/skill/understand/test_scan_project.test.mjs b/tests/skill/understand/test_scan_project.test.mjs index 7618ae7ec..5c3bd0d30 100644 --- a/tests/skill/understand/test_scan_project.test.mjs +++ b/tests/skill/understand/test_scan_project.test.mjs @@ -248,6 +248,22 @@ describe('scan-project.mjs — language detection', () => { expect(r.status).toBe(0); expect(byPath(r.output, 'data.weirdext').language).toBe('weirdext'); }); + + it('supports project-level tree-sitter extension language aliases', () => { + projectRoot = setupTree({ + '.understand-anything/config.json': JSON.stringify({ + treeSitter: { + extensionLanguageMap: { + '.customts': 'typescript', + }, + }, + }, null, 2), + 'src/component.customts': 'export const x: number = 1;\n', + }); + const r = runScript(projectRoot); + expect(r.status).toBe(0); + expect(byPath(r.output, 'src/component.customts').language).toBe('typescript'); + }); }); describe('scan-project.mjs — category assignment (project-scanner.md Step 4)', () => { diff --git a/understand-anything-plugin/agents/article-analyzer.md b/understand-anything-plugin/agents/article-analyzer.md index 6b928e3da..b4aa6a7c6 100644 --- a/understand-anything-plugin/agents/article-analyzer.md +++ b/understand-anything-plugin/agents/article-analyzer.md @@ -20,6 +20,8 @@ You will receive a batch of articles as a JSON array. Each article has: You will also receive the full list of existing node IDs so you can reference them. +If the dispatch prompt includes a language directive, apply it to all natural-language fields you generate (`name`, `summary`, edge `description`). Keep IDs, schema keys, node/edge `type`, `direction`, and other enum values unchanged. + ## Task For each article in the batch, extract: diff --git a/understand-anything-plugin/agents/domain-analyzer.md b/understand-anything-plugin/agents/domain-analyzer.md index c1e693425..f2fdfad63 100644 --- a/understand-anything-plugin/agents/domain-analyzer.md +++ b/understand-anything-plugin/agents/domain-analyzer.md @@ -20,6 +20,8 @@ A full structural knowledge graph with nodes, edges, layers, and tours. Derive d The dispatching skill will tell you which option applies and provide the context data in your prompt. +If the dispatch prompt includes a language directive, apply it to all natural-language fields in the output JSON (for example: `project.description`, node `name`, node `summary`, `domainMeta.businessRules`, and edge `description`). + ## Task Analyze the provided context and produce a domain graph JSON file. @@ -114,6 +116,7 @@ Produce a JSON object with this exact structure: - Do NOT create duplicate node IDs - Do NOT create self-referencing edges - Do NOT create nodes for domains/flows that don't exist in the codebase +- Keep schema keys, node `type`, edge `type`, `direction`, IDs, and enum values in canonical English form even when output text is localized ## Writing Results diff --git a/understand-anything-plugin/agents/file-analyzer.md b/understand-anything-plugin/agents/file-analyzer.md index df701a0ad..39dca2c98 100644 --- a/understand-anything-plugin/agents/file-analyzer.md +++ b/understand-anything-plugin/agents/file-analyzer.md @@ -202,6 +202,8 @@ Use the script's metrics to inform this -- but apply judgment. **Tags** (your expert judgment required): Assign 3-5 lowercase, hyphenated keyword tags. Use the script's structural data to inform your choices. Choose from patterns like: +Use the `language` field from the extraction result as the canonical language tag when you include a language tag. Do NOT infer the language from the file extension when they differ; project-level configuration may intentionally map custom extensions (for example `.phtml`, `.view`, `.inc`) to a tree-sitter language such as `php`. In that case use `php`, not `phtml` / `view` / `inc`, in tags and summaries. + For code files: `entry-point`, `utility`, `api-handler`, `data-model`, `test`, `config`, `middleware`, `component`, `hook`, `service`, `type-definition`, `barrel`, `factory`, `singleton`, `event-handler`, `validation`, `serialization` diff --git a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts index a66064832..d7ca87fe2 100644 --- a/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts +++ b/understand-anything-plugin/packages/core/src/__tests__/language-registry.test.ts @@ -33,6 +33,19 @@ describe("LanguageRegistry", () => { expect(registry.getForFile("file.unknown")).toBeNull(); }); + it("registers extension aliases for existing languages", () => { + const registry = LanguageRegistry.createDefault(); + registry.registerExtensionAlias(".customts", "typescript"); + expect(registry.getForFile("src/component.customts")?.id).toBe("typescript"); + }); + + it("throws when aliasing an unknown language id", () => { + const registry = LanguageRegistry.createDefault(); + expect(() => registry.registerExtensionAlias(".custom", "not-a-language")).toThrow( + 'unknown language "not-a-language"', + ); + }); + it("returns null for files without extensions and no filename match", () => { const registry = new LanguageRegistry(); expect(registry.getForFile("SOMEFILE")).toBeNull(); diff --git a/understand-anything-plugin/packages/core/src/languages/language-registry.ts b/understand-anything-plugin/packages/core/src/languages/language-registry.ts index d8eb0c28a..9f4d73710 100644 --- a/understand-anything-plugin/packages/core/src/languages/language-registry.ts +++ b/understand-anything-plugin/packages/core/src/languages/language-registry.ts @@ -47,6 +47,17 @@ export class LanguageRegistry { return this.getByExtension(ext); } + registerExtensionAlias(ext: string, languageId: string): void { + const config = this.byId.get(languageId); + if (!config) { + throw new Error( + `Cannot register extension alias "${ext}": unknown language "${languageId}"`, + ); + } + const key = (ext.startsWith(".") ? ext : `.${ext}`).toLowerCase(); + this.byExtension.set(key, config); + } + getAllLanguages(): LanguageConfig[] { return [...this.byId.values()]; } diff --git a/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.test.ts b/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.test.ts index 73cd41b70..6f72359de 100644 --- a/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.test.ts +++ b/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.test.ts @@ -185,6 +185,24 @@ const double = (x) => x * 2; expect(result.functions[1].params).toEqual(["x"]); }); + it("supports extension aliases for custom project file types", async () => { + const aliasPlugin = new TreeSitterPlugin(undefined, undefined, { + extensionLanguageMap: { + ".customts": "typescript", + }, + }); + await aliasPlugin.init(); + + const code = ` +function greet(name: string): string { + return "Hello " + name; +} +`; + const result = aliasPlugin.analyzeFile("test.customts", code); + expect(result.functions).toHaveLength(1); + expect(result.functions[0].name).toBe("greet"); + }); + it("should handle a comprehensive TypeScript file", () => { const code = ` import { EventEmitter } from 'events'; diff --git a/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts b/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts index 6b453fca2..4c277028e 100644 --- a/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts +++ b/understand-anything-plugin/packages/core/src/plugins/tree-sitter-plugin.ts @@ -16,6 +16,10 @@ const require = createRequire(import.meta.url); type TreeSitterParser = import("web-tree-sitter").Parser; type TreeSitterLanguage = import("web-tree-sitter").Language; +export interface TreeSitterPluginOptions { + extensionLanguageMap?: Record; +} + /** * Config-driven tree-sitter plugin. * @@ -58,7 +62,11 @@ export class TreeSitterPlugin implements AnalyzerPlugin { * @param configs Language configurations to load * @param extractors Optional language extractors; if none provided, registers all builtin extractors */ - constructor(configs?: LanguageConfig[], extractors?: LanguageExtractor[]) { + constructor( + configs?: LanguageConfig[], + extractors?: LanguageExtractor[], + options?: TreeSitterPluginOptions, + ) { if (configs) { this.configs = configs.filter((c) => c.treeSitter); } else { @@ -87,6 +95,28 @@ export class TreeSitterPlugin implements AnalyzerPlugin { this._extensionToLang.set(".jsx", "javascript"); } + const supportedLanguageIds = new Set(langs); + supportedLanguageIds.add("tsx"); + for (const [rawExt, rawLanguageId] of Object.entries( + options?.extensionLanguageMap ?? {}, + )) { + if (typeof rawLanguageId !== "string") { + throw new Error( + `TreeSitterPlugin extension alias "${rawExt}" must map to a string language id`, + ); + } + const ext = rawExt.startsWith(".") + ? rawExt.toLowerCase() + : `.${rawExt.toLowerCase()}`; + const languageId = rawLanguageId.toLowerCase(); + if (!supportedLanguageIds.has(languageId)) { + throw new Error( + `TreeSitterPlugin extension alias "${rawExt}" targets unsupported language "${rawLanguageId}"`, + ); + } + this._extensionToLang.set(ext, languageId); + } + this.languages = langs; // Register extractors (default: all builtin extractors) diff --git a/understand-anything-plugin/skills/understand-domain/SKILL.md b/understand-anything-plugin/skills/understand-domain/SKILL.md index 0c42eeea2..6e5f4f09e 100644 --- a/understand-anything-plugin/skills/understand-domain/SKILL.md +++ b/understand-anything-plugin/skills/understand-domain/SKILL.md @@ -1,7 +1,7 @@ --- name: understand-domain description: Extract business domain knowledge from a codebase and generate an interactive domain flow graph. Works standalone (lightweight scan) or derives from an existing /understand knowledge graph. -argument-hint: "[--full]" +argument-hint: "[--full|--language ]" --- # /understand-domain @@ -13,6 +13,7 @@ Extracts business domain knowledge — domains, business flows, and process step - If a knowledge graph already exists (`.ua/knowledge-graph.json`, or the legacy `.understand-anything/knowledge-graph.json` when that directory is present), derives domain knowledge from it (cheap, no file scanning) - If no knowledge graph exists, performs a lightweight scan: file tree + entry point detection + sampled files - Use `--full` flag to force a fresh scan even if a knowledge graph exists +- Output language defaults to `.understand-anything/config.json` `outputLanguage`; override with `--language ` for this run and persist it ## Instructions @@ -92,6 +93,19 @@ fi Use `$PLUGIN_ROOT` for every reference to agent definitions in subsequent phases. +### Phase 0.5: Resolve output language + +1. Read `$PROJECT_ROOT/.understand-anything/config.json` if it exists. +2. Parse `$ARGUMENTS` for `--language `: + - If provided, set `$OUTPUT_LANGUAGE` to that value and merge `{"outputLanguage":""}` back into `config.json` while preserving existing keys. + - If not provided, use `outputLanguage` from config when present. + - If still empty, set `$OUTPUT_LANGUAGE=en`. +3. Build `$LANGUAGE_DIRECTIVE`: + +```markdown +> **Language directive**: Generate all textual content in **{language}**. This includes `project.description`, every node `name`, node `summary`, edge `description` (if present), and all free-text business/domain fields. Keep fixed schema keys, enum values, and IDs unchanged. +``` + ### Phase 1: Detect Existing Graph 1. Check if `$UA_DIR/knowledge-graph.json` exists @@ -143,7 +157,7 @@ The preprocessing script does NOT produce a domain graph — it produces **raw m ### Phase 4: Domain Analysis 1. Read the domain-analyzer agent prompt from `$PLUGIN_ROOT/agents/domain-analyzer.md` -2. Dispatch a subagent with the domain-analyzer prompt + the context from Phase 2 or 3 +2. Dispatch a subagent with the domain-analyzer prompt + the context from Phase 2 or 3 + `$LANGUAGE_DIRECTIVE` 3. The agent writes its output to `$UA_DIR/intermediate/domain-analysis.json` ### Phase 5: Validate and Save diff --git a/understand-anything-plugin/skills/understand-knowledge/SKILL.md b/understand-anything-plugin/skills/understand-knowledge/SKILL.md index 6a6670124..977284271 100644 --- a/understand-anything-plugin/skills/understand-knowledge/SKILL.md +++ b/understand-anything-plugin/skills/understand-knowledge/SKILL.md @@ -1,7 +1,7 @@ --- name: understand-knowledge description: Analyze a Karpathy-pattern LLM wiki knowledge base and generate an interactive knowledge graph with entity extraction, implicit relationships, and topic clustering. -argument-hint: "[wiki-directory]" +argument-hint: "[wiki-directory] [--language ]" --- # /understand-knowledge @@ -21,6 +21,42 @@ Detection signals: has `index.md` + multiple `.md` files with wikilinks. May hav ## Instructions +### Phase 0: Resolve target directory and output language + +1. Parse `$ARGUMENTS`: + - `--language ` is optional. + - Any remaining positional argument is the wiki directory (`$TARGET_DIR`). +2. Determine `$TARGET_DIR`: + - If a positional path argument exists, use it. + - Otherwise, use the current working directory. + - If `$TARGET_DIR` is inside a git worktree checkout, redirect to the main repository root (same behavior as `/understand`) so config and outputs use the canonical `.understand-anything/`: + ```bash + COMMON_DIR=$(git -C "$TARGET_DIR" rev-parse --git-common-dir 2>/dev/null) + GIT_DIR=$(git -C "$TARGET_DIR" rev-parse --git-dir 2>/dev/null) + if [ -n "$COMMON_DIR" ] && [ -n "$GIT_DIR" ]; then + COMMON_ABS=$(cd "$TARGET_DIR" && cd "$COMMON_DIR" 2>/dev/null && pwd -P) + GIT_ABS=$(cd "$TARGET_DIR" && cd "$GIT_DIR" 2>/dev/null && pwd -P) + if [ -n "$COMMON_ABS" ] && [ "$COMMON_ABS" != "$GIT_ABS" ]; then + MAIN_ROOT=$(dirname "$COMMON_ABS") + if [ -d "$MAIN_ROOT" ] && [ "${UNDERSTAND_NO_WORKTREE_REDIRECT:-0}" != "1" ]; then + echo "[understand-knowledge] Detected git worktree at $TARGET_DIR" + echo "[understand-knowledge] Redirecting output to main repo root: $MAIN_ROOT" + TARGET_DIR="$MAIN_ROOT" + fi + fi + fi + ``` +3. Resolve `$OUTPUT_LANGUAGE`: + - Read `$TARGET_DIR/.understand-anything/config.json` if it exists. + - If `--language ` is provided, use it and merge `{"outputLanguage":""}` into config while preserving existing keys. + - Otherwise, use `outputLanguage` from config when present. + - If still empty, default to `en`. +4. Build `$LANGUAGE_DIRECTIVE`: + +```markdown +> **Language directive**: Generate all textual content in **{language}**. Apply this to newly generated node `name`, node `summary`, and edge `description` fields. Keep schema keys, enum/type values, and IDs unchanged. +``` + ### Phase 1: DETECT 1. Determine the target directory: @@ -123,6 +159,7 @@ Dispatch `article-analyzer` subagents to extract implicit knowledge: - "Knowledge graph saved: N articles, N entities, N topics, N claims, N sources" - "N edges (N wikilink, N categorized, N implicit)" - "N layers, N tour steps" + - Write this summary in `$OUTPUT_LANGUAGE` 7. Auto-trigger the dashboard: ``` diff --git a/understand-anything-plugin/skills/understand-onboard/SKILL.md b/understand-anything-plugin/skills/understand-onboard/SKILL.md index b2c8a2f8c..16fbb3c31 100644 --- a/understand-anything-plugin/skills/understand-onboard/SKILL.md +++ b/understand-anything-plugin/skills/understand-onboard/SKILL.md @@ -1,6 +1,7 @@ --- name: understand-onboard description: Use when you need to generate an onboarding guide for new team members joining a project +argument-hint: "[--language ]" --- # /understand-onboard @@ -31,8 +32,16 @@ The knowledge graph JSON has this structure: ## Instructions 1. **Resolve the data directory `$UA_DIR`.** Run `UA_DIR=$([ -d .understand-anything ] && echo .understand-anything || echo .ua)` — this is the legacy `.understand-anything/` when it already exists, otherwise the new `.ua/`. Check that `$UA_DIR/knowledge-graph.json` exists. If not, tell the user to run `/understand` first. - -2. **Check graph freshness before using graph-derived context**: +2. **Resolve output language** + - Read `$PROJECT_ROOT/$UA_DIR/config.json` if it exists. + - Parse `$ARGUMENTS` for `--language `: + - If provided, set `$OUTPUT_LANGUAGE` to that value and merge `{"outputLanguage":""}` into `$PROJECT_ROOT/$UA_DIR/config.json` while preserving other keys. + - If not provided, use `outputLanguage` from config when present. + - If still empty, set `$OUTPUT_LANGUAGE=en`. + - This step is mandatory. Do NOT ignore `outputLanguage` when present. + - Use this directive when writing the guide: + - **Language directive**: Generate all textual content in **$OUTPUT_LANGUAGE**. Keep file paths, IDs, code identifiers, and command snippets unchanged. +3. **Check graph freshness before using graph-derived context**: - Read `project.gitCommitHash` from the graph metadata as `GRAPH_COMMIT_RAW`. Resolve it as a commit before using it in any Git diff, then compare it with `git rev-parse HEAD` and inspect project-scoped committed and working-tree changes from the project root: ```bash GRAPH_COMMIT=$(git rev-parse --verify --end-of-options "${GRAPH_COMMIT_RAW}^{commit}" 2>/dev/null) @@ -47,17 +56,17 @@ The knowledge graph JSON has this structure: - If the committed diff or any working-tree command reports project files, warn before generating the guide that onboarding content may omit those changes. Suggest: Run `/understand` to refresh the graph. - Run the commit diff only when `GRAPH_COMMIT_RAW` resolves successfully. If the graph commit or Git metadata is missing, invalid, or unavailable, give a brief best-effort warning and continue instead of blocking. -3. **Read project metadata** — use Grep or Read with a line limit to extract the `"project"` section (name, description, languages, frameworks). +4. **Read project metadata** — use Grep or Read with a line limit to extract the `"project"` section (name, description, languages, frameworks). -4. **Read layers** — Grep for `"layers"` to get the full layers array. These define the architecture and will structure the guide. +5. **Read layers** — Grep for `"layers"` to get the full layers array. These define the architecture and will structure the guide. -5. **Read the tour** — Grep for `"tour"` to get the guided walkthrough steps. These provide the recommended learning path. +6. **Read the tour** — Grep for `"tour"` to get the guided walkthrough steps. These provide the recommended learning path. -6. **Read file-level structural nodes only** — use Grep to find nodes with file-level types (`file`, `config`, `document`, `service`, `pipeline`, `table`, `schema`, `resource`, `endpoint`) in the knowledge graph. Skip function-level and class-level nodes to keep the guide high-level. Extract each node's `name`, `filePath`, `summary`, and `complexity`. +7. **Read file-level structural nodes only** — use Grep to find nodes with file-level types (`file`, `config`, `document`, `service`, `pipeline`, `table`, `schema`, `resource`, `endpoint`) in the knowledge graph. Skip function-level and class-level nodes to keep the guide high-level. Extract each node's `name`, `filePath`, `summary`, and `complexity`. -7. **Identify complexity hotspots** — from the file-level nodes, find those with the highest `complexity` values. These are areas new developers should approach carefully. +8. **Identify complexity hotspots** — from the file-level nodes, find those with the highest `complexity` values. These are areas new developers should approach carefully. -8. **Generate the onboarding guide** with these sections: +9. **Generate the onboarding guide** with these sections (all prose follows the language directive):: - **Project Overview**: name, languages, frameworks, description (from project metadata) - **Architecture Layers**: each layer's name, description, and key files (from layers + file nodes) - **Key Concepts**: important patterns and design decisions (from node summaries and tags) @@ -65,6 +74,6 @@ The knowledge graph JSON has this structure: - **File Map**: what each key file does (from file-level nodes, organized by layer) - **Complexity Hotspots**: areas to approach carefully (from complexity values) -9. Format as clean markdown -10. Offer to save the guide to `docs/ONBOARDING.md` in the project -11. Suggest the user commit it to the repo for the team +10. Format as clean markdown +11. Offer to save the guide to `docs/ONBOARDING.md` in the project +12. Suggest the user commit it to the repo for the team diff --git a/understand-anything-plugin/skills/understand/SKILL.md b/understand-anything-plugin/skills/understand/SKILL.md index f13012e0e..6624bf5dd 100644 --- a/understand-anything-plugin/skills/understand/SKILL.md +++ b/understand-anything-plugin/skills/understand/SKILL.md @@ -154,7 +154,7 @@ Determine whether to run a full analysis or incremental update. - **Stored preference wins.** If `$UA_DIR/config.json` has an `outputLanguage` field, set `$OUTPUT_LANGUAGE` to it and skip the rest. - **Otherwise detect (first run only).** Infer the predominant language of the user's conversation as an ISO 639-1 code (`$DETECTED_LANG`). If it is `en` or cannot be confidently determined, set `$OUTPUT_LANGUAGE=en` and proceed silently — no prompt (English users see no change). - **If `$DETECTED_LANG` ≠ `en`, confirm once before analyzing:** tell the user you detected `` and ask whether to generate all content in it; they press Enter/"yes" to accept, or type another language code/name to override (normalize via the friendly-name map above). If running non-interactively (no reply possible), skip the wait, use `$DETECTED_LANG`, and print a one-line notice instead of blocking. - - **Persist** the resolved `$OUTPUT_LANGUAGE` (including `en`) into `config.json` so it never re-prompts for this project. + - **Persist** the resolved `$OUTPUT_LANGUAGE` (including `en`) into `config.json` so it never re-prompts for this project. Merge `{"outputLanguage": ""}` into the existing object; do NOT overwrite or remove other keys such as `treeSitter.extensionLanguageMap`. - If `--language` IS specified: - Update `$UA_DIR/config.json` with the new language: merge `{"outputLanguage": ""}` into existing config. - Store as `$OUTPUT_LANGUAGE` for use throughout all phases. @@ -232,7 +232,7 @@ Set up and verify the `.understandignore` file before scanning. Report to the user: `[Phase 1/7] Scanning project files...` -Dispatch a subagent using the `project-scanner` agent definition (at `agents/project-scanner.md`). Append the following additional context: +Dispatch a subagent using the `project-scanner` agent definition (at `$PLUGIN_ROOT/agents/project-scanner.md`). Append the following additional context: > **Additional context from main session:** > @@ -300,7 +300,7 @@ Load `$UA_DIR/intermediate/batches.json` (produced by Phase 1.5). Iterate the `b Report: `[Phase 2/7] Analyzing files — files in batches (up to 5 concurrent)...` -For each batch, dispatch a subagent using the `file-analyzer` agent definition (at `agents/file-analyzer.md`). Run up to **5 subagents concurrently**. Append the following additional context: +For each batch, dispatch a subagent using the `file-analyzer` agent definition (at `$PLUGIN_ROOT/agents/file-analyzer.md`). Run up to **5 subagents concurrently**. Append the following additional context: > **Additional context from main session:** > @@ -390,7 +390,7 @@ After batches complete: Report to the user: `[Phase 3/7] Reviewing assembled graph...` -Dispatch a subagent using the `assemble-reviewer` agent definition (at `agents/assemble-reviewer.md`). +Dispatch a subagent using the `assemble-reviewer` agent definition (at `$PLUGIN_ROOT/agents/assemble-reviewer.md`). Pass these parameters in the dispatch prompt: @@ -418,7 +418,7 @@ After the subagent completes, read `$UA_DIR/intermediate/assemble-review.json` a Report to the user: `[Phase 4/7] Identifying architectural layers...` **Build the combined prompt template:** - 1. Use the `architecture-analyzer` agent definition (at `agents/architecture-analyzer.md`). + 1. Use the `architecture-analyzer` agent definition (at `$PLUGIN_ROOT/agents/architecture-analyzer.md`). 2. **Language context injection:** For each language detected in Phase 1 (e.g., `python`, `markdown`, `dockerfile`, `yaml`, `sql`, `terraform`, `graphql`, `protobuf`, `shell`, `html`, `css`), read the file at `./languages/.md` (e.g., `./languages/python.md`, `./languages/dockerfile.md`) and append its content after the base template under a `## Language Context` header. If the file does not exist for a detected language, skip it silently and continue. These files are in the `languages/` subdirectory next to this SKILL.md file. **Include non-code language snippets** — they provide edge patterns and summary styles for non-code files. 3. **Framework addendum injection:** For each framework detected in Phase 1 (e.g., `Django`), read the file at `./frameworks/.md` (e.g., `./frameworks/django.md`) and append its full content after the language context. If the file does not exist for a detected framework, skip it silently and continue. These files are in the `frameworks/` subdirectory next to this SKILL.md file. 4. **Output locale injection:** If `$OUTPUT_LANGUAGE` is NOT `en` (English), read the locale guidance file at `./locales/.md` (e.g., `./locales/zh.md`, `./locales/ja.md`, `./locales/ko.md`) and append its content after the framework addendums under a `## Output Language Guidelines` header. This provides language-specific guidance for tag naming conventions, summary style, and layer name translations. If the locale file does not exist for the specified language, skip silently — the `$LANGUAGE_DIRECTIVE` still applies. These files are in the `locales/` subdirectory next to this SKILL.md file. @@ -500,7 +500,7 @@ All four fields (`id`, `name`, `description`, `nodeIds`) are required. Report to the user: `[Phase 5/7] Building guided tour...` -Dispatch a subagent using the `tour-builder` agent definition (at `agents/tour-builder.md`). Append the following additional context: +Dispatch a subagent using the `tour-builder` agent definition (at `$PLUGIN_ROOT/agents/tour-builder.md`). Append the following additional context: > **Additional context from main session:** > @@ -693,7 +693,7 @@ If the script exits non-zero, read stderr, fix the script, and retry once. If `--review` IS in `$ARGUMENTS`, dispatch the LLM graph-reviewer subagent as follows: -Dispatch a subagent using the `graph-reviewer` agent definition (at `agents/graph-reviewer.md`). Append the following additional context: +Dispatch a subagent using the `graph-reviewer` agent definition (at `$PLUGIN_ROOT/agents/graph-reviewer.md`). Append the following additional context: > **Additional context from main session:** > diff --git a/understand-anything-plugin/skills/understand/build-fingerprints.mjs b/understand-anything-plugin/skills/understand/build-fingerprints.mjs index 61cbe8c37..6d24ff105 100644 --- a/understand-anything-plugin/skills/understand/build-fingerprints.mjs +++ b/understand-anything-plugin/skills/understand/build-fingerprints.mjs @@ -28,6 +28,7 @@ import { createRequire } from 'node:module'; import { dirname, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { readFileSync } from 'node:fs'; +import { readTreeSitterExtensionLanguageMap } from './config.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); // skills/understand/ -> plugin root is two dirs up @@ -49,6 +50,7 @@ try { const { TreeSitterPlugin, PluginRegistry, + LanguageRegistry, builtinLanguageConfigs, registerAllParsers, buildFingerprintStore, @@ -72,14 +74,29 @@ async function main() { ); } + const validLanguageIds = new Set(builtinLanguageConfigs.map((config) => config.id)); + validLanguageIds.add('tsx'); + const treeSitterExtensionLanguageMap = readTreeSitterExtensionLanguageMap( + projectRoot, + { validLanguageIds }, + ); + // Create tree-sitter plugin with all configs that have WASM grammars, // mirroring extract-structure.mjs so the baseline matches the comparison // logic used during auto-updates. const tsConfigs = builtinLanguageConfigs.filter((c) => c.treeSitter); - const tsPlugin = new TreeSitterPlugin(tsConfigs); + const tsPlugin = new TreeSitterPlugin(tsConfigs, undefined, { + extensionLanguageMap: treeSitterExtensionLanguageMap, + }); await tsPlugin.init(); - const registry = new PluginRegistry(); + const languageRegistry = LanguageRegistry.createDefault(); + for (const [ext, languageId] of Object.entries(treeSitterExtensionLanguageMap)) { + // tsx is a synthetic grammar key for tree-sitter selection — it is + // NOT a LanguageRegistry id, so map it to typescript. + languageRegistry.registerExtensionAlias(ext, languageId === 'tsx' ? 'typescript' : languageId); + } + const registry = new PluginRegistry(languageRegistry); registry.register(tsPlugin); registerAllParsers(registry); diff --git a/understand-anything-plugin/skills/understand/compute-batches.mjs b/understand-anything-plugin/skills/understand/compute-batches.mjs index ed5d09338..5c4480ac5 100644 --- a/understand-anything-plugin/skills/understand/compute-batches.mjs +++ b/understand-anything-plugin/skills/understand/compute-batches.mjs @@ -31,6 +31,7 @@ import { readFile } from 'node:fs/promises'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { createRequire } from 'node:module'; +import { readTreeSitterExtensionLanguageMap } from './config.mjs'; /** * Chunk size for parallel file I/O. Bounded so a 15k-file repo doesn't try @@ -50,7 +51,13 @@ try { } catch { core = await import(pathToFileURL(resolve(PLUGIN_ROOT, 'packages/core/dist/index.js')).href); } -const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers, resolveUaDir } = core; +const { + TreeSitterPlugin, + PluginRegistry, + LanguageRegistry, + builtinLanguageConfigs, + registerAllParsers, +} = core; import Graph from 'graphology'; import louvain from 'graphology-communities-louvain'; @@ -65,10 +72,24 @@ import louvain from 'graphology-communities-louvain'; async function extractExports(projectRoot, codeFiles) { let registry; try { + const validLanguageIds = new Set(builtinLanguageConfigs.map((config) => config.id)); + validLanguageIds.add('tsx'); + const treeSitterExtensionLanguageMap = readTreeSitterExtensionLanguageMap( + projectRoot, + { validLanguageIds }, + ); const tsConfigs = builtinLanguageConfigs.filter(c => c.treeSitter); - const tsPlugin = new TreeSitterPlugin(tsConfigs); + const tsPlugin = new TreeSitterPlugin(tsConfigs, undefined, { + extensionLanguageMap: treeSitterExtensionLanguageMap, + }); await tsPlugin.init(); - registry = new PluginRegistry(); + const languageRegistry = LanguageRegistry.createDefault(); + for (const [ext, languageId] of Object.entries(treeSitterExtensionLanguageMap)) { + // tsx is a synthetic grammar key for tree-sitter selection — it is + // NOT a LanguageRegistry id, so map it to typescript. + languageRegistry.registerExtensionAlias(ext, languageId === 'tsx' ? 'typescript' : languageId); + } + registry = new PluginRegistry(languageRegistry); registry.register(tsPlugin); registerAllParsers(registry); } catch (err) { diff --git a/understand-anything-plugin/skills/understand/config.mjs b/understand-anything-plugin/skills/understand/config.mjs new file mode 100644 index 000000000..12c07617c --- /dev/null +++ b/understand-anything-plugin/skills/understand/config.mjs @@ -0,0 +1,119 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; + +// --------------------------------------------------------------------------- +// Resolve the config file path. Try the current `.ua/config.json` first +// (the resolved data directory for new projects and already-analysed +// projects that use `.ua/`), then fall back to the legacy +// `.understand-anything/config.json`. This avoids the data-directory +// switch triggered by `resolveUaDirName()` in core when only a +// config-only `.understand-anything/` directory exists — the mere +// presence of that directory would cause all subsequent graph, metadata, +// and fingerprint reads to look in `.understand-anything/` instead of +// the existing `.ua/`, silently abandoning incremental state. +// --------------------------------------------------------------------------- +export const CONFIG_RELATIVE_PATH = '.understand-anything/config.json'; + +function resolveConfigPath(projectRoot) { + const uaPath = join(projectRoot, '.ua', 'config.json'); + if (existsSync(uaPath)) return uaPath; + return join(projectRoot, CONFIG_RELATIVE_PATH); +} + +function normalizeExtension(rawExtension) { + if (typeof rawExtension !== 'string') { + throw new Error( + `treeSitter.extensionLanguageMap key must be a string, got ${typeof rawExtension}`, + ); + } + const trimmed = rawExtension.trim().toLowerCase(); + if (!trimmed) { + throw new Error('treeSitter.extensionLanguageMap key must not be empty'); + } + const normalized = trimmed.startsWith('.') ? trimmed : `.${trimmed}`; + if (normalized === '.') { + throw new Error('treeSitter.extensionLanguageMap key must not be "."'); + } + return normalized; +} + +function normalizeLanguageId(rawLanguageId) { + if (typeof rawLanguageId !== 'string') { + throw new Error( + `treeSitter.extensionLanguageMap value must be a string, got ${typeof rawLanguageId}`, + ); + } + const normalized = rawLanguageId.trim().toLowerCase(); + if (!normalized) { + throw new Error('treeSitter.extensionLanguageMap value must not be empty'); + } + return normalized; +} + +function asObject(value, fieldPath) { + if (value === undefined) return {}; + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`${fieldPath} must be an object`); + } + return value; +} + +/** + * Read `.understand-anything/config.json` and return normalized tree-sitter + * extension aliases. + * + * Expected JSON shape: + * { + * "treeSitter": { + * "extensionLanguageMap": { + * ".foo": "typescript" + * } + * } + * } + */ +export function readTreeSitterExtensionLanguageMap( + projectRoot, + options = {}, +) { + const configPath = resolveConfigPath(projectRoot); + if (!existsSync(configPath)) return {}; + + const raw = readFileSync(configPath, 'utf-8'); + + let parsed; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error( + `Invalid JSON in ${CONFIG_RELATIVE_PATH}: ${err.message}`, + ); + } + + const root = asObject(parsed, 'config'); + const treeSitter = asObject(root.treeSitter, 'treeSitter'); + const extensionLanguageMap = asObject( + treeSitter.extensionLanguageMap, + 'treeSitter.extensionLanguageMap', + ); + + const validLanguageIds = options.validLanguageIds ?? null; + const normalized = {}; + + for (const [rawExtension, rawLanguageId] of Object.entries(extensionLanguageMap)) { + const ext = normalizeExtension(rawExtension); + const languageId = normalizeLanguageId(rawLanguageId); + if (validLanguageIds && !validLanguageIds.has(languageId)) { + throw new Error( + `treeSitter.extensionLanguageMap["${rawExtension}"] points to unknown language "${rawLanguageId}"`, + ); + } + if (Object.prototype.hasOwnProperty.call(normalized, ext) && normalized[ext] !== languageId) { + throw new Error( + `Conflicting language mappings for extension "${ext}" in treeSitter.extensionLanguageMap`, + ); + } + normalized[ext] = languageId; + } + + return normalized; +} diff --git a/understand-anything-plugin/skills/understand/extract-import-map.mjs b/understand-anything-plugin/skills/understand/extract-import-map.mjs index ef1034b4a..15f6b9591 100644 --- a/understand-anything-plugin/skills/understand/extract-import-map.mjs +++ b/understand-anything-plugin/skills/understand/extract-import-map.mjs @@ -38,6 +38,7 @@ import { dirname, resolve, join, posix } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; +import { readTreeSitterExtensionLanguageMap } from './config.mjs'; /** * Read a list of files concurrently while preserving result order. Failures @@ -81,7 +82,13 @@ try { core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href); } -const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core; +const { + TreeSitterPlugin, + PluginRegistry, + LanguageRegistry, + builtinLanguageConfigs, + registerAllParsers, +} = core; // --------------------------------------------------------------------------- // Path helpers @@ -561,7 +568,7 @@ const NODENEXT_REWRITES = { * For NodeNext-style imports (`./foo.js` where only `./foo.ts` exists), apply * the source-extension rewrite — see NODENEXT_REWRITES above. */ -function probeWithExtensions(basePath, fileSet) { +function probeWithExtensions(basePath, fileSet, extraExtProbes = null) { if (!basePath) return null; // Exact match (import already had an extension that resolves on disk) if (fileSet.has(basePath)) return basePath; @@ -588,6 +595,16 @@ function probeWithExtensions(basePath, fileSet) { const candidate = basePath + ext; if (fileSet.has(candidate)) return candidate; } + + // Probe configured extension aliases for TS/JS languages (e.g. .customts + // mapped to typescript → a.customts importing ./b should find b.customts). + if (extraExtProbes) { + for (const ext of extraExtProbes) { + const candidate = basePath + ext; + if (fileSet.has(candidate)) return candidate; + } + } + return null; } @@ -600,7 +617,7 @@ function probeWithExtensions(basePath, fileSet) { * targets are anchored at THAT tsconfig's directory, matching the way the * TypeScript compiler resolves nested project configs. */ -export function resolveTsJsImport(rawImport, file, ctx) { +export function resolveTsJsImport(rawImport, file, ctx, extraExtProbes = null) { if (!rawImport || typeof rawImport !== 'string') return null; const src = rawImport.trim(); if (!src) return null; @@ -610,7 +627,7 @@ export function resolveTsJsImport(rawImport, file, ctx) { // Relative imports: ./foo, ../foo — tsconfig has no bearing here. if (src.startsWith('./') || src.startsWith('../')) { const base = resolveRelative(importerDir, src); - return probeWithExtensions(base, ctx.fileSet); + return probeWithExtensions(base, ctx.fileSet, extraExtProbes); } // tsconfig path aliases. Walk up from the importer to find the nearest @@ -648,7 +665,7 @@ export function resolveTsJsImport(rawImport, file, ctx) { ); // Defensive: tsconfig targets shouldn't escape the project root. if (candidate.startsWith('..')) continue; - const probed = probeWithExtensions(candidate, ctx.fileSet); + const probed = probeWithExtensions(candidate, ctx.fileSet, extraExtProbes); if (probed) return probed; } } @@ -1735,7 +1752,7 @@ function resolveImport(imp, file, ctx) { const lang = file.language; const src = imp.source; if (TS_JS_LANGS.has(lang)) { - const out = resolveTsJsImport(src, file, ctx); + const out = resolveTsJsImport(src, file, ctx, ctx.tsJsExtProbes ?? null); return out ? [out] : []; } if (lang === 'python') { @@ -1812,6 +1829,13 @@ async function main() { throw new Error('Invalid input: must contain projectRoot and files array'); } + const validLanguageIds = new Set(builtinLanguageConfigs.map((config) => config.id)); + validLanguageIds.add('tsx'); + const treeSitterExtensionLanguageMap = readTreeSitterExtensionLanguageMap( + projectRoot, + { validLanguageIds }, + ); + // Create tree-sitter plugin with all configs that have WASM grammars. // // WHY graceful init: the most likely real-world failure mode is the WASM @@ -1825,9 +1849,17 @@ async function main() { let treeSitterReady = false; try { const tsConfigs = builtinLanguageConfigs.filter(c => c.treeSitter); - const tsPlugin = new TreeSitterPlugin(tsConfigs); + const tsPlugin = new TreeSitterPlugin(tsConfigs, undefined, { + extensionLanguageMap: treeSitterExtensionLanguageMap, + }); await tsPlugin.init(); - registry = new PluginRegistry(); + const languageRegistry = LanguageRegistry.createDefault(); + for (const [ext, languageId] of Object.entries(treeSitterExtensionLanguageMap)) { + // tsx is a synthetic grammar key for tree-sitter selection — it is + // NOT a LanguageRegistry id, so map it to typescript. + languageRegistry.registerExtensionAlias(ext, languageId === 'tsx' ? 'typescript' : languageId); + } + registry = new PluginRegistry(languageRegistry); registry.register(tsPlugin); registerAllParsers(registry); treeSitterReady = true; @@ -1844,6 +1876,21 @@ async function main() { // `buildResolutionContext`. const ctx = await buildResolutionContext(projectRoot, files); + // Inject configured extension aliases into the TS/JS resolution probes + // so that imports between custom-extension files are found (e.g. + // .customts → typescript: a.customts importing ./b must resolve to + // b.customts, not only b.ts/b.tsx/b.js). + const tsJsCustomExts = []; + for (const [ext, langId] of Object.entries(treeSitterExtensionLanguageMap)) { + if (TS_JS_LANGS.has(langId)) { + tsJsCustomExts.push(ext); + tsJsCustomExts.push(`/index${ext}`); + } + } + if (tsJsCustomExts.length > 0) { + ctx.tsJsExtProbes = tsJsCustomExts; + } + const importMap = {}; let filesWithImports = 0; let totalEdges = 0; diff --git a/understand-anything-plugin/skills/understand/extract-structure.mjs b/understand-anything-plugin/skills/understand/extract-structure.mjs index 32cbf29b6..f99ca176c 100644 --- a/understand-anything-plugin/skills/understand/extract-structure.mjs +++ b/understand-anything-plugin/skills/understand/extract-structure.mjs @@ -20,10 +20,8 @@ import { createRequire } from 'node:module'; import { dirname, resolve, join } from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs'; -import { - analyzeFileWithOutcomes, - buildResult as buildExtractResult, -} from './extract-structure-result.mjs'; +import { analyzeFileWithOutcomes,buildResult as buildExtractResult } from './extract-structure-result.mjs'; +import { readTreeSitterExtensionLanguageMap } from './config.mjs'; export { analyzeFileWithOutcomes, @@ -50,7 +48,13 @@ try { core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href); } -const { TreeSitterPlugin, PluginRegistry, builtinLanguageConfigs, registerAllParsers } = core; +const { + TreeSitterPlugin, + PluginRegistry, + LanguageRegistry, + builtinLanguageConfigs, + registerAllParsers, +} = core; // --------------------------------------------------------------------------- // Main @@ -71,13 +75,28 @@ async function main() { throw new Error('Invalid input: must contain projectRoot and batchFiles array'); } + const validLanguageIds = new Set(builtinLanguageConfigs.map((config) => config.id)); + validLanguageIds.add('tsx'); + const treeSitterExtensionLanguageMap = readTreeSitterExtensionLanguageMap( + projectRoot, + { validLanguageIds }, + ); + // Create tree-sitter plugin with all configs that have WASM grammars const tsConfigs = builtinLanguageConfigs.filter(c => c.treeSitter); - const tsPlugin = new TreeSitterPlugin(tsConfigs); + const tsPlugin = new TreeSitterPlugin(tsConfigs, undefined, { + extensionLanguageMap: treeSitterExtensionLanguageMap, + }); await tsPlugin.init(); // Create registry and register tree-sitter + all non-code parsers - const registry = new PluginRegistry(); + const languageRegistry = LanguageRegistry.createDefault(); + for (const [ext, languageId] of Object.entries(treeSitterExtensionLanguageMap)) { + // tsx is a synthetic grammar key for tree-sitter selection — it is + // NOT a LanguageRegistry id, so map it to typescript for registration. + languageRegistry.registerExtensionAlias(ext, languageId === 'tsx' ? 'typescript' : languageId); + } + const registry = new PluginRegistry(languageRegistry); registry.register(tsPlugin); registerAllParsers(registry); diff --git a/understand-anything-plugin/skills/understand/merge-batch-graphs.py b/understand-anything-plugin/skills/understand/merge-batch-graphs.py index 1c7746bfb..0ae06362a 100644 --- a/understand-anything-plugin/skills/understand/merge-batch-graphs.py +++ b/understand-anything-plugin/skills/understand/merge-batch-graphs.py @@ -1058,6 +1058,98 @@ def recover_imports_from_scan( return recovered, lines +def normalize_file_language_metadata_from_scan( + assembled: dict[str, Any], + scan_result_path: Path, +) -> tuple[int, list[str]]: + """Normalize file-node language tags from the deterministic scan result. + + File-analyzer agents sometimes infer the language from the filename + extension when writing free-form tags/summaries. Project-level tree-sitter + aliases deliberately override that extension (for example `.phtml` -> `php`), + so the final graph should reflect the scan result's language, not the raw + suffix. + """ + if not scan_result_path.is_file(): + return 0, [f" language metadata normalization skipped — {scan_result_path.name} not found"] + + try: + scan = json.loads(scan_result_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as e: + return 0, [f" language metadata normalization skipped — could not parse {scan_result_path.name}: {e}"] + + files = scan.get("files") + if not isinstance(files, list): + return 0, [f" language metadata normalization skipped — no files array in {scan_result_path.name}"] + + language_by_path: dict[str, str] = {} + for file_meta in files: + if not isinstance(file_meta, dict): + continue + path = file_meta.get("path") + language = file_meta.get("language") + if isinstance(path, str) and path and isinstance(language, str) and language: + language_by_path[path] = language.lower() + + normalized = 0 + generic_summary_rewrites = 0 + + for node in assembled.get("nodes", []): + if not isinstance(node, dict) or node.get("type") != "file": + continue + file_path = node.get("filePath") + if not isinstance(file_path, str) or not file_path: + continue + language = language_by_path.get(file_path) + if not language: + continue + + suffix = Path(file_path).suffix.lower().lstrip(".") + if not suffix or suffix == language: + continue + + changed = False + tags = node.get("tags") + if isinstance(tags, list): + new_tags: list[Any] = [] + removed_suffix_tag = False + has_language_tag = False + for tag in tags: + if isinstance(tag, str): + tag_l = tag.lower() + if tag_l == language: + has_language_tag = True + if tag_l == suffix: + removed_suffix_tag = True + changed = True + continue + new_tags.append(tag) + if removed_suffix_tag and not has_language_tag: + new_tags.append(language) + changed = True + node["tags"] = new_tags + + summary = node.get("summary") + if isinstance(summary, str) and summary: + # Common fallback emitted by file-analyzer when it does not inspect + # the source deeply: "code phtml file at path." Preserve richer + # summaries; only correct this generic extension-derived wording. + pattern = re.compile(rf"\b(code|script|markup)\s+{re.escape(suffix)}\s+file\s+at\b", re.IGNORECASE) + updated_summary = pattern.sub(rf"\1 {language} file at", summary) + if updated_summary != summary: + node["summary"] = updated_summary + generic_summary_rewrites += 1 + changed = True + + if changed: + normalized += 1 + + return normalized, [ + f" Normalized {normalized} file node(s) using scan-result languages", + f" Rewrote {generic_summary_rewrites} generic extension-derived summary field(s)", + ] + + # ── Main ────────────────────────────────────────────────────────────────── def main() -> None: @@ -1231,6 +1323,14 @@ def main() -> None: report.append("Imports edge recovery:") report.extend(recovery_report) + normalized_language_nodes, language_metadata_report = normalize_file_language_metadata_from_scan( + assembled, scan_result_path + ) + if language_metadata_report: + report.append("") + report.append("File language metadata normalization:") + report.extend(language_metadata_report) + # Print report print("", file=sys.stderr) for line in report: diff --git a/understand-anything-plugin/skills/understand/scan-project.mjs b/understand-anything-plugin/skills/understand/scan-project.mjs index c6e3d6774..7839a51ec 100644 --- a/understand-anything-plugin/skills/understand/scan-project.mjs +++ b/understand-anything-plugin/skills/understand/scan-project.mjs @@ -71,6 +71,7 @@ import { writeFileSync, } from 'node:fs'; import { spawnSync } from 'node:child_process'; +import { readTreeSitterExtensionLanguageMap } from './config.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); // skills/understand/ -> plugin root is two dirs up @@ -92,7 +93,7 @@ try { core = await import(pathToFileURL(resolve(pluginRoot, 'packages/core/dist/index.js')).href); } -const { createIgnoreFilter, resolveUaDir } = core; +const { createIgnoreFilter, builtinLanguageConfigs,resolveUaDir } = core; // --------------------------------------------------------------------------- // Language detection @@ -232,7 +233,7 @@ const LANGUAGE_BY_FILENAME = Object.freeze({ * extension. Downstream consumers rely on this field always being a string * (see project-scanner.md Step 3 "Fallback" note). */ -export function detectLanguage(filePath) { +export function detectLanguage(filePath, extensionLanguageMap = null) { const base = basename(filePath); const ext = extname(filePath).toLowerCase(); @@ -248,6 +249,9 @@ export function detectLanguage(filePath) { if (dotKey && LANGUAGE_BY_EXT[dotKey]) return LANGUAGE_BY_EXT[dotKey]; if (ext) { + if (extensionLanguageMap && extensionLanguageMap[ext]) { + return extensionLanguageMap[ext]; + } const byExt = LANGUAGE_BY_EXT[ext]; if (byExt) return byExt; // Unknown extension → drop the leading dot, lowercase. Never null. @@ -764,7 +768,14 @@ async function main() { (!rel.startsWith('.ua/') && !rel.startsWith('.understand-anything/')), ); - // 2. Filter via createIgnoreFilter (defaults + .understandignore + CLI excludes). + const validLanguageIds = new Set(builtinLanguageConfigs.map((config) => config.id)); + validLanguageIds.add('tsx'); + const treeSitterExtensionLanguageMap = readTreeSitterExtensionLanguageMap( + projectRoot, + { validLanguageIds }, + ); + + // 2. Filter via createIgnoreFilter (defaults + user .understandignore + CLI --exclude). // Build a defaults-only filter in parallel to count user-driven drops. const combined = createIgnoreFilter(projectRoot, excludePatterns); const userIgnoresPresent = hasUserIgnoreFile(projectRoot) || excludePatterns.length > 0; @@ -828,7 +839,7 @@ async function main() { updateContentDigest(contentHash, rel, scanned.bytes); fileEntries.push({ path: rel, - language: detectLanguage(rel), + language: detectLanguage(rel, treeSitterExtensionLanguageMap), sizeLines: scanned.sizeLines, fileCategory: detectCategory(rel), });