ローカルファイルをAIに分析させる「Local Navigator」
【更新履歴】
・2026/2/19 バージョン1.0公開。
・2026/2/19 バージョン1.1公開。
Local Navigator ユーザーマニュアル
Local Navigatorは、あなたのローカル環境にある大量のファイル(ソースコードやドキュメント)を、外部のAI(ChatGPT, Claude, Geminiなど)を使って効率的に解析・編集するための「コンテキスト・エンジニアリングツール」です。
Pythonなどの環境構築は不要。ブラウザ(Chrome/Edge)とこのHTMLファイルだけで動作し、ローカルで動作するAIモデル(Transformers.js)によるベクトル検索までサポートしています。
はじめに:このツールの目的
通常のRAG(検索拡張生成)や、AIへのファイルアップロードには以下の課題があります。
トークン制限: 全ファイルを送るとコンテキストが溢れる、または課金が高額になる。
検索精度: 単なるキーワード検索では、機能的な関連性(例:「認証」で「Login.ts」を探すなど)が見つからない。
プライバシー/手間: サーバーを立てたり、APIキーを管理するのが面倒。
LocalNavigator3は、**「AIにファイル構造(地図)だけを渡し、AIが必要なファイルだけを指名して読む」**というアプローチでこれらを解決します。さらに、ブラウザ内で完結するベクトル検索により、高度な意味検索も実現しました。
セットアップと起動
ファイルの準備:
提供されたHTMLコードを、index.html という名前でPC上の任意の場所に保存します。
ブラウザで開く:
index.html を Google Chrome または Microsoft Edge で開きます。
注意: SafariやFirefoxでは「File System Access API」が完全にはサポートされていないため、動作しない場合があります。
初回ロード(重要):
起動直後、ブラウザは自動的に軽量AIモデル(約30MB〜)のダウンロードを開始します。
画面右上の「VECTOR AI」バッジが点灯するまで数秒〜数分お待ちください。
一度ダウンロードされれば、次回以降はキャッシュが使われるため一瞬で起動します。
画面構成
左パネル(エクスプローラー):
1. フォルダを開く: 解析対象のプロジェクトフォルダを選択します。
ファイル構造: ツリー形式でディレクトリ構造が表示されます。
オプション: スマート読込やベクトル検索のON/OFFが可能です。
右パネル(インタラクション):
STEP 1: AIへの要望を入力し、最初の指示プロンプトを作成します。
STEP 2: AIからの返答(コマンド)を貼り付け、ファイルを読み込んで次のプロンプトを作成します。
使用ワークフロー
このツールは、**「Web上のAI(脳)」と「ローカルのあなた(手足)」**を繋ぐブリッジとして機能します。
Step 0: フォルダの選択
左上の [1. フォルダを開く] ボタンをクリックします。
解析したいプロジェクトのルートフォルダを選択します。
ブラウザが「ファイルの表示権限」を求めてくるので、「許可」 をクリックしてください。
ファイルパスのベクトル化(意味付け)がバックグラウンドで走ります。完了するとステータスバーに「準備完了」と表示されます。
Step 1: 要望の入力とプロンプト生成
右側の STEP 1 エリアにあるテキストボックスに、やりたいことを入力します。
例:「認証周りのコードを読んで、セキュリティ上の脆弱性がないかチェックして」
例:「src/components の中のReactコンポーネントをVue.jsに書き換える方法を教えて」
[プロンプト生成 »] ボタンを押します。
下に生成された黒いボックスをクリックして、テキストをコピーします。
Step 2: AIとの対話(ループ)
AIに送信:
コピーしたテキストを、ChatGPT / Claude / Gemini などのチャット欄に貼り付けて送信します。
AIの返答をコピー:
AIはファイルの中身を知らないため、「では、src/auth/login.ts を読み込んでください(READ: src/auth/login.ts)」のように返してきます。
このAIの返答テキストを、すべてコピーしてください。
ツールで実行:
LocalNavigator3に戻り、STEP 2 のテキストボックスに貼り付け、[実行 & 次のプロンプト生成 »] を押します。
ツールは自動的に READ: や SEARCH: コマンドを抽出し、指定されたファイルの中身や検索結果を取得します。
挨拶文(「分かりました」など)は自動削除されます。
次のプロンプトをAIへ:
生成された「ファイルの中身入りプロンプト」をクリックしてコピーし、再びAIのチャット欄に貼り付けて送信します。
AIが「解決しました」と回答するまで、このStep 2の手順を繰り返してください。
高度な機能
ベクトル検索 (Neural Search)
このツールには all-MiniLM-L6-v2 モデルが内蔵されています。
AIがファイルパスを知らない場合でも、AIに SEARCH: ログイン処理 とコマンドを出させれば、ツールはファイル名に「ログイン」が含まれていなくても、意味的に近い AuthService.ts や SessionManager.js などを発見してAIに提示します。
スマート読込 (Smart Read)
左下のオプション [スマート読込] がONの場合(デフォルト)、300行を超える巨大なファイルは、「先頭200行 + 末尾50行」 だけを切り出してAIに渡します。
これにより、トークン消費を抑えつつ、ファイルの概要と重要なロジック(冒頭のimportや定義、末尾のexportなど)をAIに把握させることができます。
トラブルシューティング
Q. フォルダを選択しても何も起きない / エラーになる
ブラウザの権限設定を確認してください。また、システム保護されたフォルダ(Windowsフォルダなど)はアクセスできない場合があります。
Q. ベクトル検索が動かない(AIモデルロードエラー)
お使いのブラウザやハードウェアが WebGPU / WebAssembly に対応していない可能性があります。その場合でも、ツールは自動的に「キーワード一致検索」モードに切り替わって動作します。
Q. AIが READ: コマンドを出してくれない
Step 1で生成されるプロンプトには「ファイルを読むときは READ: パス と出力して」という指示が含まれていますが、AIによっては無視して勝手にコードを生成し始めることがあります。その場合は、「ファイルの中身を確認してから回答してください」とAIに追加で指示してください。
⚠️ 注意事項
データプライバシー:
このツールはファイルを外部サーバーにアップロードしません。AIモデルのダウンロード以外で外部通信は行わず、ファイル解析はすべてあなたのブラウザ内(ローカル)で行われます。ただし、**生成されたプロンプトをChatGPT等に貼り付けた際、その内容は各AIサービスのプライバシーポリシーに従って扱われます。**機密情報の扱いにはご注意ください。
・ダウンロードされる方はこちら。↓
・ソースコードはこちら。↓
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Local Navigator 1.1</title>
<script type="module">
import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.16.0';
env.allowLocalModels = false;
env.useBrowserCache = true;
window.AI_PIPELINE = null;
window.AI_MODEL_STATUS = "init";
window.loadModel = async () => {
try {
updateStatus("AIモデルをダウンロード中... (初回のみ数分かかります)");
window.AI_PIPELINE = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
window.AI_MODEL_STATUS = "ready";
updateStatus("AIモデル準備完了: ベクトル検索が可能です");
document.getElementById('indicator-ai').classList.add('active');
} catch (e) {
console.error(e);
window.AI_MODEL_STATUS = "error";
updateStatus("AIモデルのロードに失敗しました (WebGPU/WASM非対応の可能性)");
}
};
function updateStatus(msg) {
const el = document.getElementById('sys-msg');
if(el) el.innerText = msg;
}
window.loadModel();
</script>
<style>
:root { --bg:#080808; --panel:#141414; --accent:#00ffcc; --text:#e0e0e0; --border:#333; --warn:#ffcc00; --danger:#ff4466; }
body { margin:0; background:var(--bg); color:var(--text); font-family:'Segoe UI', sans-serif; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
#header { height:50px; border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; background:#000; }
.logo { font-weight:bold; color:var(--accent); font-size:18px; letter-spacing:1px; display:flex; align-items:center; gap:10px; }
.badge { font-size:10px; padding:2px 6px; border-radius:4px; background:#333; color:#888; }
.badge.active { background:rgba(0, 255, 204, 0.2); color:var(--accent); border:1px solid var(--accent); }
#main { flex:1; display:flex; overflow:hidden; }
#left { width:320px; background:var(--panel); border-right:1px solid var(--border); display:flex; flex-direction:column; padding:15px; gap:15px; font-size:12px; }
#file-tree { flex:1; overflow-y:auto; border:1px solid #222; padding:8px; color:#999; white-space:pre; background:#0a0a0a; font-family:monospace; }
#right { flex:1; display:flex; flex-direction:column; background:#0e0e0e; padding:20px; gap:20px; overflow-y:auto; }
.step-card { background:#1a1a1a; border:1px solid var(--border); padding:20px; border-radius:6px; position:relative; transition:0.3s; }
.step-card.active { border-color:var(--accent); box-shadow:0 0 15px rgba(0,255,204,0.05); }
/* 修正点: pointer-events: none を削除し、操作可能にしました */
.step-card.inactive { opacity:0.6; filter:grayscale(0.8); }
.step-tag { position:absolute; top:-10px; left:15px; background:var(--bg); color:var(--accent); padding:0 8px; font-size:11px; font-weight:bold; border:1px solid var(--accent); }
textarea { width:100%; height:80px; background:#050505; border:1px solid #333; color:#fff; padding:12px; font-family:monospace; box-sizing:border-box; resize:vertical; font-size:13px; }
textarea:focus { border-color:var(--accent); outline:none; }
.btn { background:#222; color:#ccc; border:1px solid #444; padding:10px 20px; cursor:pointer; font-size:12px; transition:0.2s; border-radius:4px; font-weight:bold; }
.btn:hover { background:var(--accent); color:#000; border-color:var(--accent); }
.btn-primary { background:rgba(0, 255, 204, 0.15); color:var(--accent); border:1px solid var(--accent); width:100%; }
.result-box { margin-top:15px; background:#111; border-left:3px solid var(--accent); padding:10px; font-family:monospace; font-size:11px; color:#aaa; cursor:pointer; max-height:250px; overflow-y:auto; white-space:pre-wrap; }
.result-box:hover { background:#181818; color:#fff; }
.result-box::after { content:'クリックしてコピー'; display:block; text-align:right; font-size:10px; color:var(--accent); margin-top:8px; opacity:0.7; }
.status-bar { font-size:11px; color:#666; display:flex; justify-content:space-between; }
#sys-msg { color:var(--warn); animation: pulse 2s infinite; }
@keyframes pulse { 0% { opacity:0.7; } 50% { opacity:1; } 100% { opacity:0.7; } }
</style>
</head>
<body>
<div id="header">
<div class="logo">
Local Navigator <span style="font-size:10px; opacity:0.7;">NEURAL EDITION</span>
</div>
<div style="display:flex; gap:10px;">
<span id="indicator-fs" class="badge">FILE SYSTEM</span>
<span id="indicator-ai" class="badge">VECTOR AI</span>
</div>
</div>
<div id="main">
<div id="left">
<button class="btn btn-primary" onclick="app.selectFolder()">1. フォルダを開く</button>
<div id="folder-name" style="color:#fff; font-weight:bold; text-align:center;">未選択</div>
<div class="status-bar">
<span id="stat-files">0 ファイル</span>
<span id="stat-vecs">0 ベクトル</span>
</div>
<div style="margin-top:10px; font-size:11px;">ファイル構造:</div>
<div id="file-tree"></div>
<div style="margin-top:auto; border-top:1px solid #333; padding-top:10px;">
<div style="font-size:10px; color:#888; margin-bottom:5px;">オプション</div>
<label style="display:flex; align-items:center; gap:5px; cursor:pointer;"><input type="checkbox" id="opt-smart" checked> スマート読込 (先頭200行+末尾50行)</label>
<label style="display:flex; align-items:center; gap:5px; cursor:pointer;"><input type="checkbox" id="opt-vector" checked> ベクトル検索を有効化</label>
<button class="btn" style="width:100%; margin-top:10px; font-size:10px;" onclick="location.reload()">リセット</button>
</div>
</div>
<div id="right">
<div style="font-size:11px;" id="sys-msg">システム待機中...</div>
<div class="step-card active" id="step-1">
<div class="step-tag">STEP 1: 要望の入力</div>
<p style="font-size:12px; color:#888; margin:0 0 10px 0;">AIに依頼したい内容を入力してください。(例:「認証機能のコードを解析して」)</p>
<textarea id="user-req" placeholder="ここに要望を入力..."></textarea>
<div style="margin-top:10px; text-align:right;">
<button class="btn" onclick="app.genPrompt()">プロンプト生成 »</button>
</div>
<div id="out-prompt-1" class="result-box" style="display:none;" onclick="app.copyText(this)"></div>
</div>
<div class="step-card inactive" id="step-2">
<div class="step-tag">STEP 2: AI返答の処理</div>
<p style="font-size:12px; color:#888; margin:0 0 10px 0;">Web上のAIからの返答(READ: xxx や SEARCH: xxx を含むもの)を貼り付けてください。</p>
<textarea id="ai-resp" placeholder="AIの返答をここにペースト..."></textarea>
<div style="margin-top:10px; text-align:right;">
<button class="btn" onclick="app.processResponse()">実行 & 次のプロンプト生成 »</button>
</div>
<div id="out-prompt-2" class="result-box" style="display:none;" onclick="app.copyText(this)"></div>
</div>
</div>
</div>
<script type="module">
const app = {
dirHandle: null,
fileMap: {},
vectors: [],
treeText: "",
selectFolder: async () => {
try {
app.dirHandle = await window.showDirectoryPicker();
document.getElementById('indicator-fs').classList.add('active');
document.getElementById('folder-name').innerText = app.dirHandle.name;
app.fileMap = {};
app.vectors = [];
const lines = [];
updateStatus("ファイルをスキャン中...");
await app.scanDir(app.dirHandle, "", lines, 0);
app.treeText = lines.join('\n');
document.getElementById('file-tree').innerText = app.treeText;
document.getElementById('stat-files').innerText = `${Object.keys(app.fileMap).length} ファイル`;
if (window.AI_MODEL_STATUS === 'ready') {
updateStatus("ファイルパスのベクトル化を実行中...");
await app.vectorizePaths();
updateStatus("準備完了。");
} else {
updateStatus("AIモデルのロード待ちです... ロード後に自動でベクトル化されます。");
}
// フォルダ選択後はStep 1にフォーカス
app.switchStep(1);
} catch(e) { console.error(e); alert("キャンセルされました"); }
},
scanDir: async (handle, path, lines, depth) => {
const entries = [];
for await (const entry of handle.values()) entries.push(entry);
entries.sort((a,b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : (a.kind === 'directory' ? -1 : 1)));
for (let i=0; i<entries.length; i++) {
const entry = entries[i];
if(['node_modules','.git','bin','obj','.vs','dist','build','vendor'].includes(entry.name)) continue;
const isLast = (i === entries.length - 1);
const prefix = " ".repeat(depth) + (isLast ? "+-- " : "|-- ");
if (entry.kind === 'file') {
const ext = entry.name.split('.').pop().toLowerCase();
if(!['exe','dll','png','jpg','jpeg','gif','zip','mp4','pdf'].includes(ext)) {
lines.push(prefix + entry.name);
const fullPath = path + entry.name;
app.fileMap[fullPath] = entry;
app.vectors.push({ path: fullPath, embedding: null });
}
} else {
lines.push(prefix + entry.name + "/");
await app.scanDir(entry, path + entry.name + "/", lines, depth + 1);
}
}
},
vectorizePaths: async () => {
if (!window.AI_PIPELINE) return;
const total = app.vectors.length;
for (let i = 0; i < total; i++) {
const item = app.vectors[i];
const textToEmbed = item.path.replace(/[\/\._-]/g, ' ');
const output = await window.AI_PIPELINE(textToEmbed, { pooling: 'mean', normalize: true });
item.embedding = output.data;
if (i % 10 === 0) updateStatus(`ベクトル生成中... ${i}/${total}`);
}
document.getElementById('stat-vecs').innerText = `${total} 埋め込み済`;
},
searchFiles: async (query) => {
if (!window.AI_PIPELINE || !document.getElementById('opt-vector').checked) {
return app.vectors.filter(v => v.path.toLowerCase().includes(query.toLowerCase())).map(v => ({ item: v, score: 1 }));
}
const qOutput = await window.AI_PIPELINE(query, { pooling: 'mean', normalize: true });
const qVec = qOutput.data;
const results = app.vectors.map(item => {
if (!item.embedding) return { item, score: 0 };
let dot = 0;
for (let i = 0; i < qVec.length; i++) {
dot += qVec[i] * item.embedding[i];
}
if (item.path.toLowerCase().includes(query.toLowerCase())) {
dot += 0.3;
}
return { item, score: dot };
});
return results.sort((a,b) => b.score - a.score).slice(0, 5);
},
genPrompt: () => {
const req = document.getElementById('user-req').value.trim();
if(!req) return alert("要望を入力してください");
const prompt = `
[システム指示]
あなたは高度なコーディングアシスタント「LocalNavigator」です。
現在、ユーザーのローカルファイルに直接アクセスできませんが、以下の「ファイル構造」は見えています。
[ユーザーの要望]
${req}
[プロトコル]
1. 要望とファイル構造を分析してください。
2. 情報を得るために、以下のコマンドを出力してください(挨拶や前置きは不要です)。
- ファイルの中身を読む: "READ: パス"
- ファイルを探す(パスが不明な場合): "SEARCH: 検索キーワード"
3. 情報が十分なら、回答を行ってください。
[ファイル構造]
${app.treeText}
`;
app.showResult('out-prompt-1', prompt);
// Step 2へフォーカスを移すが、Step 1は操作可能なままにする
app.switchStep(2);
},
processResponse: async () => {
const raw = document.getElementById('ai-resp').value;
if(!raw.trim()) return;
updateStatus("AI返答を解析中...");
const lines = raw.split('\n');
const commands = { reads: [], searches: [] };
lines.forEach(line => {
const r = line.match(/READ:\s*(.+)/);
const s = line.match(/SEARCH:\s*(.+)/);
if (r) commands.reads.push(r[1].trim().replace(/['"`]/g,''));
if (s) commands.searches.push(s[1].trim().replace(/['"`]/g,''));
});
let output = "[システム] 以下の情報を取得しました:\n\n";
let hasData = false;
if (commands.searches.length > 0) {
hasData = true;
for (const q of commands.searches) {
const hits = await app.searchFiles(q);
output += `=== "SEARCH: ${q}" の結果 ===\n`;
if(hits.length===0) output += "該当なし\n";
else hits.forEach(h => output += `- ${h.item.path} (関連度: ${h.score.toFixed(2)})\n`);
output += "\n";
}
}
if (commands.reads.length > 0) {
hasData = true;
const useSmart = document.getElementById('opt-smart').checked;
for (const path of commands.reads) {
const cleanPath = Object.keys(app.fileMap).find(p => p.endsWith(path)) || path;
if (app.fileMap[cleanPath]) {
try {
const f = await app.fileMap[cleanPath].getFile();
const text = await f.text();
const lines = text.split('\n');
let content = text;
if (useSmart && lines.length > 300) {
content = lines.slice(0, 200).join('\n') +
`\n\n... (中略: ${lines.length - 250}行) ...\n\n` +
lines.slice(-50).join('\n');
}
output += `=== FILE: ${cleanPath} ===\n${content}\n======================\n\n`;
} catch(e) {
output += `[エラー] ${cleanPath} を読み込めませんでした: ${e.message}\n`;
}
} else {
output += `[エラー] ファイルが見つかりません: ${path}\n`;
}
}
}
if (!hasData) {
output = "[システム] ファイル読み込み/検索コマンドが検出されませんでした。\nこれが最終回答であれば、ユーザーへの返答として扱ってください。\nもし操作が必要なら、正しい形式 (READ: xxx) で出力してください。";
} else {
output += "\n[指示] 上記の情報を元に回答を作成するか、さらに必要な情報があれば READ/SEARCH コマンドを出力してください。";
}
app.showResult('out-prompt-2', output);
document.getElementById('ai-resp').value = "";
updateStatus("処理完了。次のプロンプトをコピーしてください。");
},
switchStep: (n) => {
// 両方のステップを「見た目」だけで切り替えるが、操作はブロックしない
document.getElementById('step-1').className = n===1 ? 'step-card active' : 'step-card inactive';
document.getElementById('step-2').className = n===2 ? 'step-card active' : 'step-card inactive';
},
showResult: (id, text) => {
const el = document.getElementById(id);
el.innerText = text;
el.style.display = 'block';
},
copyText: (el) => {
navigator.clipboard.writeText(el.innerText);
const bg = el.style.backgroundColor;
el.style.backgroundColor = '#333';
setTimeout(() => el.style.backgroundColor = '', 200);
}
};
window.app = app;
function updateStatus(msg) {
const el = document.getElementById('sys-msg');
if(el) el.innerText = msg;
}
</script>
</body>
</html>

コメント