ローカルファイルをAIに分析させる「Local Navigator」

【更新履歴】

・2026/2/19 バージョン1.0公開。
・2026/2/19 バージョン1.1公開。(ベクトル検索)
・2026/2/18 バージョン1.2公開。(手動モード廃止)
・2026/2/18 バージョン1.3公開。
・2026/8/3 バージョン1.4公開。

画像
バージョン1.1の画面
画像
バージョン1.2の画面
画像
バージョン1.4の画面

プロダクトPR文


・「Local Navigator」は、
 あなたのローカル環境にあるプロジェクトファイルを、
 Anthropicの最新AIモデル(Claude等)と連携させて
 効率的に解析・編集・実行するための
 「自律型コンテキスト・エンジニアリングツール」です。

・旧バージョンでは、
 ユーザー自身がAIとツールの間でプ
 ロンプトを往復させる必要がありました。

・しかし、最新バージョンでは
 AIが自律的にコマンドを発行し、
 ローカル環境で直接作業を行う
 「エージェント・ループ」へと進化しました。

・ファイルを読む(READ)、
 書き換える(WRITE)、
 探す(SEARCH)、
 そしてシェルコマンドを実行する(EXEC)
 という一連の開発サイクルを
 AIが全自動で行います。

・ブラウザ上でNode.js環境を再現する
 WebContainer技術を搭載しており、
 環境構築なしで即座に
 テストやビルドの実行が可能です。

・Transformers.jsを利用した
 完全ローカルのベクトル検索により、
 ファイル名だけでなく
 「意味」による高度なコンテキスト抽出を実現しています。

・面倒なファイル手動アップロードや
 コピペ作業から解放され、
 AIに「目的」を伝えるだけで開発が進む、
 次世代のローカル開発体験を提供します。


■このツールの目的

・通常のRAG(検索拡張生成)や、
 AIへのファイルアップロードには

 「トークン制限により
  全ファイルを送れない」

 「単なるキーワード検索では
  機能的な関連性が見つからない」
 
 といった課題がありました。

・「Local Navigator」では、
 「AIにファイル構造(地図)だけを渡し、
  AIが必要なファイルだけを指名して読む」
  というアプローチで
  これらの問題を解決します。

・さらに今バージョンからは、
 AI自身が「検索して中身を読み、
 修正し、ターミナルで動作確認する」
 という人間と同じ思考プロセス (ループ) を
 ブラウザ内で完結させることに成功しました。


《 操作説明書 》


(1) セットアップと起動

・本ツールの目玉機能である
 WebContainer (ブラウザ内Node.js) を動かすには、
 ブラウザがCross-Origin-Isolatedな状態である必要があります。

・起動時には、
 提供されたPythonスクリプトを使用し、
 ターミナルで以下のコマンドを実行して
 サーバーを起動してください。

python serve.py


・これにより、必要なセキュリティヘッダーが付与された状態で
 ポート8888にローカルサーバーが立ち上がります。

・その後、ブラウザ (ChromeやEdge推奨) で
 http://localhost:8888/ にアクセスしてください。

(2) 画面構成

・画面は大きくわけて
 左パネルと、右パネルとに分かれています。

 左パネル  (エクスプローラー) :

 ・解析対象のプロジェクトフォルダを開くボタン、
  読み込まれたファイル構造のツリー表示、
  そして各種権限 (Vector Search、Allow Exec、Allow Write) の
  ON/OFFスイッチが配置されています。

 右パネル (インタラクション):

・AIとのチャット履歴が表示されるメインエリア、
 コマンドの実行結果が流れる黒いターミナルペイン、
 そして最下部にAnthropic APIキーの入力欄と
 プロンプト(指示文)入力用のテキストエリアがあります。

(3) 使用ワークフロー

・準備として、右下にある入力欄に
 Anthropic APIキー(sk-ant-...)を入力します。

・次に、左上の「フォルダを開く」ボタンをクリックし、
 解析したいプロジェクトのルートフォルダを選択します。

・初回ロード時には、
 ブラウザ内で動作する軽量AIモデルのダウンロードが
 自動的に開始され、完了すると
 ファイルパスのベクトル化 (意味付け) が行われます。

・右下のテキストボックスに、
 「このコードのバグを直して」
 「テストを実行して」といった
 やりたいことを入力し、
 SEND(送信)ボタンを押します。

・AIが自律ループを開始し、
 「必要な情報をREADやSEARCHで集める」
 「コードの修正が必要ならWRITEする」
 「動作確認が必要ならEXECする」
 という作業を最大10回反復してタスクを完遂し、
 あなたに報告します。

(4) 高度な機能

ベクトル検索 ( Vector Search ):

・all-MiniLM-L6-v2モデルが内蔵されています。

・AIがSEARCH コマンドを出した際、
 単なるキーワード一致だけでなく、
 意味的に近いファイルを自動的に発見してAIに提示します。

WebContainer ( NODE.JS ):

・プロジェクトフォルダ内のファイルを
 ブラウザ上の仮想Node.js環境にマウントします。

・AIが EXEC コマンド ( 例:npm test ) を発行すると、
 ターミナルペインで実際にコマンドが実行され、
 その出力結果がAIにフィードバックされます。

(5) トラブルシューティングと注意事項

◆WebContainerが起動しない、
 またはターミナルに警告が出る場合は、
 必ず serve.py を経由してアクセスしているか
 (HTTPSまたはCOOP/COEPヘッダーがあるか)
 を確認してください。

◆AIがコードを勝手に書き換えるのが不安な場合は、
 左下の「Allow Write」のチェックを外してください。

 これにより書き込み権限がブロックされ、
 AIは読み込みと提案のみを行うようになります。

◆本ツールはAnthropic APIに直接通信を行います。

 フォルダ全体をアップロードするわけではありませんが、
 AIが READ コマンドで読み込んだファイルの内容や実行結果は
 API経由で送信されるため、機密情報の取り扱いと
 APIの利用料金にはご注意ください。


・ダウンロードされる方はこちら。↓

 


・1.4のソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LocalNavigator 1.4</title>
<script>
    // log()/updateBadge() must be defined here, in a classic (non-module) script that
    // runs before the ES module scripts below. Each module script block gets its own
    // top-level scope, so a function declared in one module is invisible to another -
    // and module scripts execute (in document order) only after this classic script has
    // already run, so declaring these as ordinary global functions here is what lets the
    // AI-loader module (immediately below) call log()/updateBadge() safely.
    function log(tag, msg) {
        const el = document.getElementById('sys-log');
        if (el) el.innerText = `[${tag}] ${msg}`;
        console.log(`[${tag}]`, msg);
    }
    function updateBadge(id, active) {
        const el = document.getElementById('badge-' + id);
        if (!el) return;
        if (active) el.classList.add('active'); else el.classList.remove('active');
    }
</script>
<script type="module">
    import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
    env.allowLocalModels = false; env.useBrowserCache = true;
    window.AI = { pipeline: null, status: 'init', load: async () => {
        try {
            log('SYS', 'AIモデル(Vector) ダウンロード中...');
            window.AI.pipeline = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
            window.AI.status = 'ready';
            log('SYS', 'AIモデル準備完了');
            updateBadge('ai', true);
            // フォルダが先に読み込まれていた場合、ここでベクトル化をキックする(競合状態の修正)
            if (window.app && window.app.files && Object.keys(window.app.files).length > 0) {
                window.app.vectorize();
            }
        }
        catch(e) { log('ERR', 'AIモデルロード失敗: ' + e); }
    }};
    window.AI.load();
</script>
<script>
    if (!window.crossOriginIsolated) {
        const script = document.createElement('script');
        script.src = 'https://unpkg.com/coi-serviceworker/coi-serviceworker.js';
        document.head.appendChild(script);
    }
</script>
<style>
  :root {
    --bg:#f4f4f6;        /* app background */
    --panel:#ffffff;     /* side panel / input area */
    --surface:#f8f8fa;   /* subtle recessed surface (tool blocks, terminal) */
    --border:#dcdce1;    /* visible but soft dividers */
    --accent:#6d28d9;    /* violet-700: strong contrast on white */
    --accent-soft:rgba(109,40,217,0.08);
    --text:#18181b;      /* primary text, near-black */
    --text-2:#3f3f46;    /* secondary text */
    --text-3:#71717a;    /* muted/caption text */
    --green:#047857; --green-soft:rgba(4,120,87,0.1);
    --red:#b91c1c;   --red-soft:rgba(185,28,28,0.1);
    --yellow:#b45309; --yellow-soft:rgba(180,83,9,0.1);
    --mono:'Menlo',monospace;
  }
  * { box-sizing:border-box; } body { margin:0; background:var(--bg); color:var(--text); font-family:system-ui,sans-serif; height:100vh; display:flex; flex-direction:column; overflow:hidden; }
  
  /* Layout */
  #header { height:50px; border-bottom:1px solid var(--border); display:flex; align-items:center; padding:0 20px; justify-content:space-between; background:var(--panel); }
  .logo { font-weight:900; letter-spacing:1px; color:var(--text); display:flex; gap:10px; align-items:center; }
  .badge { font-size:10px; padding:2px 8px; border-radius:100px; background:var(--surface); color:var(--text-3); transition:0.3s; border:1px solid var(--border); }
  .badge.active { background:var(--accent-soft); color:var(--accent); border-color:var(--accent); }
  
  #main { flex:1; display:flex; overflow:hidden; }
  #left { width:300px; background:var(--panel); border-right:1px solid var(--border); display:flex; flex-direction:column; }
  #center { flex:1; display:flex; flex-direction:column; background:var(--bg); position:relative; }
  #terminal-pane { height:200px; background:var(--surface); border-top:1px solid var(--border); padding:10px; font-family:var(--mono); font-size:12px; overflow:auto; color:var(--text-2); display:none; }
  
  /* Components */
  .btn { background:var(--surface); border:1px solid var(--border); color:var(--text); padding:8px 12px; border-radius:6px; cursor:pointer; font-size:12px; transition:0.2s; display:inline-flex; align-items:center; gap:6px; }
  .btn:hover { background:var(--accent); color:#fff; border-color:var(--accent); }
  .btn:disabled { opacity:0.5; cursor:not-allowed; }
  .btn-primary { background:var(--accent); color:#fff; border-color:var(--accent); }
  input, textarea { background:#fff; border:1px solid var(--border); color:var(--text); padding:10px; border-radius:6px; font-family:var(--mono); width:100%; outline:none; }
  input::placeholder, textarea::placeholder { color:var(--text-3); }
  input:focus, textarea:focus { border-color:var(--accent); box-shadow:0 0 0 3px var(--accent-soft); }
  
  /* Tree */
  #file-tree { flex:1; overflow:auto; padding:10px; font-family:var(--mono); font-size:11px; color:var(--text-2); line-height:1.6; }
  .tree-item { cursor:pointer; padding:2px 6px; border-radius:4px; white-space:nowrap; }
  .tree-item:hover { background:var(--accent-soft); color:var(--accent); }
  .tree-dir { color:var(--accent); font-weight:bold; }

  /* Chat */
  #chat { flex:1; overflow-y:auto; padding:20px; display:flex; flex-direction:column; gap:20px; scroll-behavior:smooth; }
  .msg { max-width:90%; line-height:1.6; font-size:13px; animation:fadeIn 0.3s; }
  .msg.user { align-self:flex-end; background:var(--accent-soft); border:1px solid rgba(109,40,217,0.25); padding:12px; border-radius:12px 12px 0 12px; color:var(--text); }
  .msg.assistant { align-self:flex-start; max-width:100%; }
  .msg-role { font-size:10px; color:var(--text-3); margin-bottom:4px; font-weight:bold; letter-spacing:1px; }
  
  /* Tools */
  .tool-block { margin-top:8px; border:1px solid var(--border); border-radius:6px; overflow:hidden; background:var(--surface); font-family:var(--mono); font-size:11px; }
  .tool-head { padding:6px 10px; background:#eeeef1; border-bottom:1px solid var(--border); display:flex; justify-content:space-between; align-items:center; cursor:pointer; }
  .tool-body { padding:10px; color:var(--text-2); white-space:pre-wrap; max-height:300px; overflow:auto; display:none; background:#fff; }
  .tool-body.open { display:block; }
  .tag { font-size:9px; padding:2px 5px; border-radius:3px; font-weight:bold; }
  .tag.READ { background:var(--green-soft); color:var(--green); }
  .tag.WRITE { background:var(--red-soft); color:var(--red); }
  .tag.EXEC { background:var(--yellow-soft); color:var(--yellow); }
  .tag.SEARCH { background:var(--accent-soft); color:var(--accent); }

  /* Input Area */
  #input-area { padding:20px; background:var(--panel); border-top:1px solid var(--border); display:flex; gap:10px; align-items:flex-end; }
  
  @keyframes fadeIn { from { opacity:0; transform:translateY(5px); } to { opacity:1; transform:translateY(0); } }
  
  /* Utils */
  .loading-dots:after { content:'.'; animation:dots 1.5s infinite; }
  @keyframes dots { 33%{content:'..'} 66%{content:'...'} }
  #sys-log { font-size:10px; color:var(--text-3); padding:0 20px; height:20px; display:flex; align-items:center; border-top:1px solid var(--border); background:var(--panel); }
</style>
</head>
<body>

<div id="header">
    <div class="logo">
        <svg width="20" height="20" viewBox="0 0 24 24" fill="var(--accent)"><path d="M12 2L2 7l10 5 10-5-10-5zm0 9l2.5-1.25L12 8.5l-2.5 1.25L12 11zm0 2.5l-5-2.5-5 2.5L12 22l10-8.5-5-2.5-5 2.5z"/></svg>
        Local Navigator<span style="font-size:10px; opacity:0.5; font-weight:normal;">GENESIS</span>
    </div>
    <div style="display:flex; gap:8px;">
        <span id="badge-fs" class="badge">FS</span>
        <span id="badge-ai" class="badge">VECTOR</span>
        <span id="badge-wc" class="badge">NODE.JS</span>
    </div>
</div>

<div id="main">
    <div id="left">
        <div style="padding:15px; border-bottom:1px solid var(--border);">
            <button class="btn btn-primary" onclick="app.selectFolder()" style="width:100%; justify-content:center;">📂 フォルダを開く</button>
        </div>
        <div id="file-tree"></div>
        <div style="padding:15px; border-top:1px solid var(--border); font-size:11px; display:flex; flex-direction:column; gap:8px;">
            <label><input type="checkbox" id="opt-vector" checked> Vector Search</label>
            <label><input type="checkbox" id="opt-exec" checked> Allow Exec (Beta)</label>
            <label><input type="checkbox" id="opt-write" checked> Allow Write</label>
        </div>
    </div>
    
    <div id="center">
        <div id="chat">
            <div style="text-align:center; color:var(--text-3); margin-top:50px;">
                <h2 style="color:var(--text);">Dev Environment Ready</h2>
                <p>APIキーを設定して、開発を開始してください。</p>
            </div>
        </div>
        
        <div id="terminal-pane"></div>

        <div id="input-area">
            <div style="display:flex; flex-direction:column; gap:5px; flex:1;">
                <input type="password" id="api-key" placeholder="Anthropic API Key (sk-ant-...)" style="font-size:11px; padding:5px;">
                <textarea id="user-input" rows="2" placeholder="「このコードのバグを直して」「テストを実行して」..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();app.send();}"></textarea>
            </div>
            <button id="send-btn" class="btn btn-primary" onclick="app.send()" style="height:100%;">SEND</button>
        </div>
        <div id="sys-log">System Ready.</div>
    </div>
</div>

<script type="module">
// Dynamic import + try/catch, on purpose: a static top-level `import` would abort this
// entire module (and everything defined in it, including `app`) if the CDN URL ever
// fails to resolve - which is exactly what happened with an old pinned version. Isolating
// it like this means a WebContainer/CDN failure only disables EXEC, not the whole app.
let WebContainer = null;
try {
    ({ WebContainer } = await import('https://cdn.jsdelivr.net/npm/@webcontainer/api@1.6.4/index.js'));
} catch (e) {
    console.error('WebContainer module failed to load:', e);
    log('ERR', 'WebContainerモジュールの読み込みに失敗(EXEC機能は無効化されます): ' + e.message);
}

// --- UTILS: WebContainer / FS helpers ---

// WebContainer.mount() requires a nested tree ({dir:{directory:{file:{file:{contents}}}}}),
// but paths in app.files are flat ("src/index.js"). This converts a flat
// { path: contents } map into the nested structure WebContainer expects.
function buildMountTree(fileContents) {
    const root = {};
    for (const [path, content] of Object.entries(fileContents)) {
        const parts = path.split('/').filter(Boolean);
        let cur = root;
        for (let i = 0; i < parts.length - 1; i++) {
            const part = parts[i];
            if (!cur[part]) cur[part] = { directory: {} };
            cur = cur[part].directory;
        }
        cur[parts[parts.length - 1]] = { file: { contents: content } };
    }
    return root;
}

// FileSystemDirectoryHandle.getFileHandle() cannot create nested files directly
// ("a/b/c.js" fails if "a/b" doesn't exist yet). This walks/creates the
// intermediate directory handles first.
async function getOrCreateFileHandle(rootDirHandle, path, opts) {
    const parts = path.split('/').filter(Boolean);
    let dir = rootDirHandle;
    for (let i = 0; i < parts.length - 1; i++) {
        dir = await dir.getDirectoryHandle(parts[i], { create: true });
    }
    return await dir.getFileHandle(parts[parts.length - 1], opts);
}

function escapeHtml(str) {
    return String(str)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

// --- CORE APPLICATION ---
const app = {
    dirHandle: null,
    files: {},      // path -> handle
    vectors: [],    // {path, embedding}
    wcInstance: null,
    wcMounted: false,
    history: [],    // Chat history
    maxIter: 10,
    
    // 1. Initialize & Folder Selection
    selectFolder: async () => {
        try {
            app.dirHandle = await window.showDirectoryPicker();
            updateBadge('fs', true);
            app.files = {};
            app.vectors = [];
            document.getElementById('file-tree').innerHTML = '<div style="padding:10px;">Scanning...</div>';
            
            // Scan Files
            const treeHtml = await app.scanDir(app.dirHandle, "");
            document.getElementById('file-tree').innerHTML = treeHtml;
            log('FS', `${Object.keys(app.files).length} files scanned.`);

            // Init WebContainer (if supported)
            if (!app.wcMounted) await app.initWebContainer();

            // Vectorize (Background)
            if (window.AI.status === 'ready') app.vectorize();
            
            // Reset Chat
            document.getElementById('chat').innerHTML = `
                <div class="msg assistant">
                    <div class="msg-role">SYSTEM</div>
                    プロジェクト <b>${app.dirHandle.name}</b> をロードしました。<br>
                    解析、修正、実行が可能です。
                </div>`;
                
        } catch(e) { log('ERR', e); }
    },

    scanDir: async (handle, path) => {
        let html = "";
        const entries = [];
        for await (const entry of handle.values()) entries.push(entry);
        entries.sort((a,b) => b.kind.localeCompare(a.kind) || a.name.localeCompare(b.name));

        for (const entry of entries) {
            if (['node_modules','.git','dist','.next'].includes(entry.name)) continue;
            const fullPath = path ? `${path}/${entry.name}` : entry.name;
            
            if (entry.kind === 'file') {
                app.files[fullPath] = entry;
                app.vectors.push({ path: fullPath, embedding: null });
                const safePath = escapeHtml(fullPath).replace(/'/g, "\\'");
                html += `<div class="tree-item" onclick="app.previewFile('${safePath}')">📄 ${escapeHtml(entry.name)}</div>`;
            } else {
                html += `<div class="tree-item tree-dir">📂 ${escapeHtml(entry.name)}</div>`;
                html += `<div style="padding-left:15px; border-left:1px solid #333;">${await app.scanDir(entry, fullPath)}</div>`;
            }
        }
        return html;
    },

    // 2. WebContainer (Node.js in Browser)
    initWebContainer: async () => {
        try {
            log('SYS', 'WebContainer Booting...');
            app.wcInstance = await WebContainer.boot();

            // Mount the initially scanned files (built into a proper nested tree)
            const fileContents = {};
            for (const [path, handle] of Object.entries(app.files)) {
                const file = await handle.getFile();
                fileContents[path] = await file.text();
            }
            if (Object.keys(fileContents).length > 0) {
                await app.wcInstance.mount(buildMountTree(fileContents));
            }

            updateBadge('wc', true);
            app.wcMounted = true;
            log('SYS', 'WebContainer Ready. (Node.js available)');
            document.getElementById('terminal-pane').style.display = 'block';
        } catch(e) {
            log('ERR', 'WebContainer not supported (Needs HTTPS & COOP/COEP headers): ' + e);
            document.getElementById('terminal-pane').innerHTML = "⚠️ WebContainer requires HTTPS and Cross-Origin Isolation.";
        }
    },

    // 3. Vector Search Logic
    vectorize: async () => {
        if (!window.AI.pipeline) return;
        log('AI', 'Vectorizing file paths...');
        for (const item of app.vectors) {
            // Path + Basic Content hints (filename words)
            const text = item.path.replace(/[\/\._-]/g, ' '); 
            const out = await window.AI.pipeline(text, { pooling: 'mean', normalize: true });
            item.embedding = out.data;
        }
        log('AI', 'Vector Index Built.');
    },

    search: async (query) => {
        const useVector = document.getElementById('opt-vector').checked;

        // Vector Search チェックが外れている場合、またはAI未準備の場合はキーワード検索のみにフォールバック
        if (!useVector || !window.AI.pipeline) {
            const q = query.toLowerCase();
            return app.vectors
                .map(v => ({ path: v.path, score: v.path.toLowerCase().includes(q) ? 1 : 0 }))
                .filter(r => r.score > 0)
                .sort((a,b) => b.score - a.score)
                .slice(0,5);
        }

        const qOut = await window.AI.pipeline(query, { pooling: 'mean', normalize: true });
        const qVec = qOut.data;
        
        const results = app.vectors.map(v => {
            if(!v.embedding) return {path:v.path, score:0};
            let dot = 0;
            for(let i=0; i<qVec.length; i++) dot += qVec[i] * v.embedding[i];
            if(v.path.includes(query)) dot += 0.5; // Keyword boost
            return {path:v.path, score:dot};
        });
        return results.sort((a,b)=>b.score - a.score).slice(0,5);
    },

    // 4. Main Agent Loop
    send: async () => {
        const input = document.getElementById('user-input');
        const sendBtn = document.getElementById('send-btn');
        const key = document.getElementById('api-key').value;
        const text = input.value.trim();
        if(!text || !key) return alert("API Key and Message required.");

        if (sendBtn && sendBtn.disabled) return; // 二重実行(多重送信)防止

        input.value = "";
        app.addMsg("USER", text, "user");
        app.history.push({ role: "user", content: text });

        input.disabled = true;
        if (sendBtn) { sendBtn.disabled = true; sendBtn.textContent = "..."; }

        try {
            await app.runAgentLoop();
        } catch (e) {
            log('ERR', 'Agent loop failed: ' + e.message);
            app.addMsg("SYSTEM", `エラーが発生しました: ${e.message}`, "assistant");
        } finally {
            input.disabled = false;
            if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = "SEND"; }
            input.focus();
        }
    },

    runAgentLoop: async () => {
        const key = document.getElementById('api-key').value;
        // Build System Prompt
        const treeTxt = Object.keys(app.files).join('\n');
        const sysPrompt = `
あなたは自律型開発エージェントです。ユーザーのローカル環境にアクセスできます。
以下のツール(コマンド)を使用できます。回答はJSONではなく、思考過程の後にコマンド行を出力する形式です。

[FILE STRUCTURE]
${treeTxt}

[COMMANDS]
- READ: path          (ファイルを読む)
- WRITE: path content (ファイルを書き換える/作る。全置換)
- EXEC: command       (シェルコマンドを実行。例: npm test, node script.js)
- SEARCH: keyword     (ファイルを探す)

[RULES]
1. 必要な情報をREAD/SEARCHで集める。
2. コードの修正が必要ならWRITEする。
3. 動作確認が必要ならEXECする。
4. 最終的にユーザーへ報告する。
5. 1ターンに複数のコマンドを出力可。

Example output:
"ファイルを調べます。
READ: src/index.js
SEARCH: auth"
`;

        // Loop
        let iter = 0;
        while(iter < app.maxIter) {
            iter++;
            log('SYS', `Agent Loop ${iter}...`);
            
            // Call API
            const response = await app.callClaude(key, sysPrompt, app.history);
            app.history.push({ role: "assistant", content: response });
            
            // Parse & Execute
            const commands = app.parseCommands(response);
            const toolResults = await app.executeTools(commands);
            
            // Display
            app.addMsg("AGENT", response, "assistant", toolResults);

            if(commands.length === 0) break; // No commands = Final Answer

            // Feed results back
            const feedback = `[TOOL RESULTS]\n${toolResults.map(r => r.result).join('\n')}`;
            app.history.push({ role: "user", content: feedback });

            if (iter === app.maxIter) {
                log('SYS', `最大反復回数(${app.maxIter})に到達したため停止しました。`);
                app.addMsg("SYSTEM", `最大反復回数(${app.maxIter})に到達したため、エージェントループを停止しました。続きが必要な場合は再度指示してください。`, "assistant");
            }
        }
    },

    callClaude: async (key, system, messages) => {
        const res = await fetch('https://api.anthropic.com/v1/messages', {
            method: 'POST',
            headers: {
                'x-api-key': key,
                'anthropic-version': '2023-06-01',
                'content-type': 'application/json',
                'anthropic-dangerous-direct-browser-access': 'true'
            },
            body: JSON.stringify({
                model: 'claude-sonnet-5', // 最新モデル。必要に応じて claude-opus-4-8 等に変更可
                max_tokens: 4096,
                system: system,
                messages: messages
            })
        });
        let data;
        try {
            data = await res.json();
        } catch (e) {
            throw new Error(`APIレスポンスの解析に失敗しました (HTTP ${res.status})`);
        }
        if (data.error) throw new Error(data.error.message);
        if (!res.ok || !data.content || !data.content[0]) {
            throw new Error(`予期しないAPIレスポンス (HTTP ${res.status})`);
        }
        return data.content[0].text;
    },

    parseCommands: (text) => {
        const cmds = [];
        const lines = text.split('\n');
        let writeBuffer = null;
        let writePath = null;

        const flushWrite = () => {
            if (writeBuffer !== null) {
                cmds.push({ type: 'WRITE', arg: writePath, data: writeBuffer.trim() });
                writeBuffer = null;
                writePath = null;
            }
        };

        for (const line of lines) {
            // WRITE command parsing (Multi-line)
            if (line.startsWith('WRITE:')) {
                flushWrite(); // flush prev
                writePath = line.substring(6).trim();
                writeBuffer = "";
                continue;
            }
            if (writeBuffer !== null) {
                if (line.startsWith('END_WRITE')) {
                    flushWrite();
                    continue;
                }
                if (line.startsWith('READ:') || line.startsWith('EXEC:') || line.startsWith('SEARCH:')) {
                    // WRITEブロックを閉じて、このラインは下の単一行コマンドとして処理する
                    // (以前はここで行が破棄され、コマンドがサイレントに消失するバグがあった)
                    flushWrite();
                } else {
                    writeBuffer += line + "\n";
                    continue;
                }
            }

            // Single line commands
            if (line.startsWith('READ:')) cmds.push({ type: 'READ', arg: line.substring(5).trim() });
            else if (line.startsWith('EXEC:')) cmds.push({ type: 'EXEC', arg: line.substring(5).trim() });
            else if (line.startsWith('SEARCH:')) cmds.push({ type: 'SEARCH', arg: line.substring(7).trim() });
        }
        flushWrite();
        return cmds;
    },

    executeTools: async (cmds) => {
        const results = [];
        for (const cmd of cmds) {
            let output = "";
            try {
                if (cmd.type === 'READ') {
                    const handle = app.files[cmd.arg];
                    if(!handle) throw new Error("File not found");
                    const file = await handle.getFile();
                    output = await file.text();
                } 
                else if (cmd.type === 'SEARCH') {
                    const hits = await app.search(cmd.arg);
                    output = hits.map(h => `${h.path} (score:${h.score.toFixed(2)})`).join('\n');
                }
                else if (cmd.type === 'WRITE') {
                    if (!document.getElementById('opt-write').checked) throw new Error("Write permission denied");
                    // 1. Local FS Write (nested paths now create intermediate directories as needed)
                    let handle = app.files[cmd.arg];
                    if (!handle) {
                        handle = await getOrCreateFileHandle(app.dirHandle, cmd.arg, { create: true });
                        app.files[cmd.arg] = handle;
                        app.vectors.push({ path: cmd.arg, embedding: null });
                    }
                    const writable = await handle.createWritable();
                    await writable.write(cmd.data);
                    await writable.close();
                    
                    // 2. WebContainer Sync (built into a proper nested tree, not a flat "a/b/c" key)
                    if(app.wcMounted) {
                        await app.wcInstance.mount(buildMountTree({ [cmd.arg]: cmd.data }));
                    }
                    output = "File written successfully.";
                }
                else if (cmd.type === 'EXEC') {
                    if (!document.getElementById('opt-exec').checked) throw new Error("Exec permission denied");
                    if (!app.wcMounted) throw new Error("WebContainer not ready");
                    
                    // Quick mount of all files before exec (Sync)
                    // In production, sync only changed. Here we sync all and rebuild the nested tree.
                    const fileContents = {};
                    for(const [p, h] of Object.entries(app.files)) {
                        const f = await h.getFile();
                        fileContents[p] = await f.text();
                    }
                    await app.wcInstance.mount(buildMountTree(fileContents));

                    // Execute
                    const args = cmd.arg.split(' ');
                    const proc = await app.wcInstance.spawn(args[0], args.slice(1));
                    
                    let buf = "";
                    proc.output.pipeTo(new WritableStream({
                        write(data) { 
                            buf += data; 
                            appendToTerminal(data);
                        }
                    }));
                    await proc.exit;
                    output = buf;
                }
            } catch(e) {
                output = `Error: ${e.message}`;
            }
            results.push({ cmd: cmd, result: output });
        }
        return results;
    },

    addMsg: (role, text, cls, tools=[]) => {
        const div = document.createElement('div');
        div.className = `msg ${cls}`;
        let html = `<div class="msg-role">${escapeHtml(role)}</div><div>${escapeHtml(text).replace(/\n/g,'<br>')}</div>`;
        
        if(tools.length > 0) {
            tools.forEach(t => {
                const id = Math.random().toString(36).substr(2,9);
                html += `
                <div class="tool-block">
                    <div class="tool-head" onclick="toggle('${id}')">
                        <span class="tag ${escapeHtml(t.cmd.type)}">${escapeHtml(t.cmd.type)}</span> 
                        <span style="margin-left:10px;">${escapeHtml(t.cmd.arg)}</span>
                    </div>
                    <div class="tool-body" id="${id}">${escapeHtml(t.result)}</div>
                </div>`;
            });
        }
        div.innerHTML = html;
        document.getElementById('chat').appendChild(div);
        document.getElementById('chat').scrollTop = document.getElementById('chat').scrollHeight;
    },
    
    previewFile: async (path) => {
        const handle = app.files[path];
        const file = await handle.getFile();
        const text = await file.text();
        log('VIEW', `Previewing ${path}`);
        // Simple preview in chat
        app.addMsg("SYSTEM", `Preview: ${path}\n---\n${text.substring(0,500)}...`, "assistant");
    }
};

// Utils
// (log() / updateBadge() are defined once, globally, in the classic <script> at the
// top of <head> so both this module and the earlier AI-loader module can call them.)
function appendToTerminal(text) {
    const t = document.getElementById('terminal-pane');
    t.innerText += text;
    t.scrollTop = t.scrollHeight;
}
window.toggle = (id) => { document.getElementById(id).classList.toggle('open'); };
window.app = app;
</script>
</body>
</html>



いいなと思ったら応援しよう!

ピックアップされています

便利なツール

  • 95本

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
小ぶりなプログラムを試しに作っているんですが、 ここではその説明書きをしていこうと思います。 こういう機能をつけてみてほしいだとか要望は、 コメント欄か、Xのリプライ欄に書いてみて下さい。 ひまをみて対応します。 (未管理著作物裁定制度に定められた問い合わせも受付中。)
ローカルファイルをAIに分析させる「Local Navigator」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1