Cursor EditorでFigmaと連携する(MCP)



はい。AI時代ですね。
デザインシステムとかね、めんどいよね、自力実装。

  • Figmaにログインして、自分の名前をクリック > Settings


  • Security > Personal access tokens > Generate new token


  • frontendの開発用途ではここらへんつけとけばよいのでは(適当)
  • Generate tokenするとSettingsにtokenが表示されるので控えておきましょう


  • ~/.cursor/mcp.jsonを編集
{
  "mcpServers": {
    "figma": {
      "url": "https://mcp.figma.com/mcp"
    }
  }
}
  • Cursor Editorを起動しましょう。すでに起動していた場合は再起動が望ましいです。
  • 右上の歯車アイコンからCursor Settingsを開きます。
  • Tools & MCPが以下のようになってるはずなので、「Connect」しましょう


  • こうなってればOK


  • あとは普通にチャットでfigmaのurlはっつければ読み込んでくれるよん



デスクツアー2 ~ アプリ編 ~


極力マウスを使いたくないんですよね
手移動する時間が無駄
Windowsはマウスなしで使えるのがだいすきなんだけど、仕事柄しかたなくMacな人が使ってるアプリたちを紹介するよ

備品編はこちらから
デスクツアー1 ~ 備品編 ~ - ふるすたっくえんじにあの日記

モニター出力環境について

MacBook Pro M1 Maxをクラムシェルモードで使ってるよ
M1 Maxだと4枚まで外部モニター使えるんですねぇ + iPad ProのSideCarで5枚体制完成
Intel時代だと外付けGPUとか、USBアダプターみたいなので無理やり出してたけど素直に出力できてよき時代になりましたねぇ
そろそろMacStudio + Macbook Airにしよかな

用途

5枚もあると用途が気になるらしいね

メイン

IDEと動作確認用のブラウザやらシミュレーターやらアプリやら

Warp固定

左上

Rambox固定

右上

仕様書とかFigmaとか参考コードとかSourcetreeとか。

時計か天気かアマプラかスケジュール。
ちょっと前までShift BrowserがここにいたけどRamboxにして左上固定になったのであんまり用途がない。

ランチャー

www.raycast.com

Alt + Spaceで起動できるランチャー。
Extension豊富。複数PCで設定同期できるのでだいたいのことはこいつにまとめてる。
Window操作系もこれ一台でおkだし、ショーカットもあれこれ設定できてちょっとしたことならアプリ起動せずにこいつだけでいける。
使ってるExtensionはこいつら

AWS

複数アカウント対応。PCに設定してあるcredential見てくれる模様。
そんなに使わないけどSecretsManagerはここからValueの表示までできるからたまに使う。

Docker


Composeまとめて見れるのが便利である。ここから終了・起動もできるよ。

Format JSON

エンジニアならたまに使うよね、うん。

Sourcetree


Sourcetree自体あんまり使わないんだけどね、コミットログとかはCLIより見やすいかな。
一発で対象のリポジトリ開けるのが便利だよ

Shell


ターミナル起動せずともコマンドの実行と結果の表示・コピーができるよ。
補完が効かないのがあれだけど、それを上回る便利さ。。

Window系

全体表示やら左半分とか右半分とかWindowの位置調整系は全部Raycastに設定してるのでそれ以外

モニター管理

iPadのSidecarの縦画面化と、メインモニターが3840x1080にしか対応してないのを仮想ディスプレイで5120x1440にしてる。
betterdisplay.pro

画面切り替え

Alt + Tabで画面切り替えられるよ。設定で選択した画面へのマウスカーソル追従ができるのでカーソルなくしがちなマルチディスプレイ環境におすすめ
alt-tab-macos.netlify.app

Haze

集中したい時におすすめ。アクティブな画面以外暗くしてくれる。
HazeOver:Mac生産性向上のための注意散漫調光機能hazeover.com

ブラウザ

常時4~5つのGoogleアカウントを持っているので以下3つ組み合わせているよ。

Google Chrome

ベース。Command + Ctrl + 数字でプロファイル切り替えできるようにしてる。Macのショートカット設定でGoogle Chrome.appに、Chromeに出てるプロファイル名を設定してキー割り当てるだけ。

www.google.com

Finicky

常時4~5つのGoogleアカウントを持っているので...以下略。Ramboxと組み合わせて使うよ
Macの設定でこいつをデフォルトブラウザにしておいて
github.com

Rambox

rambox.app
StationとかShift Browserとか色々右往左往した結果。ひさびさに見たら完璧になってた。

  • Profile作成ができて、Workspace/アプリごとに個別に設定できる。

同じProfile設定したやつはセッションやらを全部共有してくれる。

  • ちゃんとWorkspace名、アプリ名設定しておくとCommand + Alt + kで一発で飛べて最高。
  • あと地味に通知がまとめて見れて便利
  • サイドバーにアプリ羅列できるだけじゃなくて、ウィンドウ分割して個別のアプリをひとつのWorkspaceに表示できる。しかもそれぞれ別のProfileが割り当てられる。

作業中に見るページなんてAsana、Notion、スプシ、GithubAWS Consoleとかプロジェクトによってだいたい固定なので全部Workspace設定しておけばいちいちChromeから開いたり整理整頓しなくて済む。神。
さらに別のProfileが割り当てられるので全クライアントのGoogle CalendarをひとつのWorkSpaceに、全SlackをまとめてひとつのWorkspaceになんてことが可能。神を超越した。

  • JSが挿入できる

StationとかShift Browserではできなかった(たぶん)。リンクとかクリックしてブラウザ開くと最後に使ったプロファイルで開かれちゃって困ってたんすよね。
JSが挿入できるのでFinickyと組み合わせるとこれも解決。
まずは`~/.finicky.js`を作りましょう
handlersのところにChromeのプロファイル名を全部書きましょう

function getRbxTag(url) {
  if (!url) return null;
  const all = url.searchParams.getAll("rbx");
  if (all.length > 0) return all[all.length - 1];
  const inner = url.searchParams.get("q");
  if (inner) {
    try {
      const innerUrl = new URL(inner);
      const innerAll = innerUrl.searchParams.getAll("rbx");
      if (innerAll.length > 0) return innerAll[innerAll.length - 1];
    } catch {}
  }
  return null;
}

export default {
  defaultBrowser: "Google Chrome",
  handlers: [
    {
      match: (url) => getRbxTag(url) === "arrvis.co.jp",
      browser: { name: "Google Chrome", profile: "arrvis.co.jp" }
    },
  ]
};

んで、Rambox側。これだけちょっとめんどいんだけどアプリ個別に設定する必要がある
アプリの設定開いてAdvanced > Inject JavaScript Codeに以下を貼り付け。TAG_VALUEは.fincky.jsのmatchのところに書いたやつね。

(function () {
  const TAG_VALUE = "arrvis.co.jp";

  const tag = (url) => {
    try {
      const u = new URL(url);
      if ((u.protocol === "http:" || u.protocol === "https:") && !u.searchParams.has("rbx")) {
        u.searchParams.append("rbx", TAG_VALUE);
      }
      return u.toString();
    } catch { return url; }
  };

  function retagLinkish(el) {
    if (!el || el.nodeType !== 1) return false;
    const candidates = [
      "href",
      "data-href",
      "data-url",
      "data-link",
      "data-target-url",
    ];
    let changed = false;
    for (const attr of candidates) {
      const v = el.getAttribute?.(attr);
      if (typeof v === "string" && v && !v.startsWith("javascript:")) {
        const newer = tag(v);
        if (newer !== v) {
          el.setAttribute(attr, newer);
          changed = true;
        }
      }
    }
    if ((el instanceof HTMLAnchorElement || el instanceof HTMLAreaElement) && el.href) {
      const newer = tag(el.href);
      if (newer !== el.href) {
        el.href = newer;
        changed = true;
      }
    }
    return changed;
  }

  function retagFromEvent(e) {
    const path = (typeof e.composedPath === "function" ? e.composedPath() : []) || [];
    for (const n of path) {
      if (retagLinkish(n)) return true;
    }
    const t = e.target;
    if (t && t.closest) {
      const el =
        t.closest("[href], [data-href], [data-url], [data-link], [data-target-url], a, area");
      if (el) return retagLinkish(el);
    }
    return false;
  }

  ["click", "auxclick", "contextmenu", "dragstart", "pointerup", "keydown"].forEach((ev) => {
    document.addEventListener(ev, (e) => {
      if (ev === "keydown" && !(e.key === "Enter" || e.key === " ")) return;
      retagFromEvent(e);
    }, true);
  });

  const _open = window.open;
  window.open = function (url, ...rest) {
    if (typeof url === "string") url = tag(url);
    return _open.call(this, url, ...rest);
  };

  const _assign = Location.prototype.assign;
  const _replace = Location.prototype.replace;
  Location.prototype.assign = function (url) {
    if (typeof url === "string") url = tag(url);
    return _assign.call(this, url);
  };
  Location.prototype.replace = function (url) {
    if (typeof url === "string") url = tag(url);
    return _replace.call(this, url);
  };

  const _aClick = HTMLAnchorElement.prototype.click;
  HTMLAnchorElement.prototype.click = function () {
    retagLinkish(this);
    return _aClick.call(this);
  };
})();

これでRamboxからデフォルトブラウザで開く > Finickyにクエリにrmbが付与される > 指定したプロファイルでChromeが開くよ




デスクツアー1 ~ 備品編 ~

ついにデスク環境晒すよ

ついでに計算したら総額80万円超えてた。(MacBook Pro除く)


アプリ編はこちらから
devdevdev.hatenablog.com

椅子


オカムラのバロンチェア。アーロンチェアより好き。10年くらいずっとこれ。
座面はクッションとメッシュどっちも持ってるけど、メッシュのほうがよいよ。
https://amzn.to/47NCFLH

足置きはこれ

https://amzn.to/3VhusrE

Devin's wikiをCursorやClaudeにも使わせる ~ MCPとGithub Actionsによる自動同期



前提

Devins's wikiしゅごい。
こいつCurrorとかClaudeにも使わせたいよね。手抜きたいよね。

  • Devin's wikiにはこんな機能があります。

docs.devin.ai

  • CursorではMCPが使えます

Cursor – Model Context Protocol (MCP)

  • Cursorは.cursor/rulesというものがあります

Cursor – Rules

  • Claudeはメモリと呼ぶらしいです

Claudeのメモリを管理する - Anthropic

CursorのChatでDevin's wikiを利用

  • DevinのAPI Keyを取得して、`~/.zprofile`とかでexportしておく
export DEVIN_API_KEY=XXXXXX
{
  "mcpServers": {
    "devin": {
      "url": "https://mcp.devin.ai/mcp",
      "headers": {
        "Authorization": "Bearer ${DEVIN_API_KEY}"
      }
    }
  }
}
  • Cursor Settingsを開いて、MCP ToolsのところがこうなってればOK


チャットでこんな感じにすれば使えるよ

@devin ask_question owner/repo "質問内容"
@devin read_wiki_structure owner/repo
@devin read_wiki_contents owner/repo

Devins'wikiの内容をCursorにも適用させる

.cursor/rulesにDevins'wikiの内容を吐き出すGithub Actions

name: Update Wiki Documentation

on:
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:

jobs:
  update-wiki:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup node
        uses: actions/setup-node@v4
        with:
          node-version-file: '.node-version'

      - name: Install dependencies
        run: |
          npm install axios

      - name: Update wiki documentation
        id: update-wiki
        run: |
          node .github/scripts/update-wiki-docs-mcp.js
        env:
          GITHUB_REPOSITORY: ${{ github.repository }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          DEVIN_API_KEY: ${{ secrets.DEVIN_API_KEY }}

      - name: Check for changes
        id: git-check
        run: |
          git diff --exit-code || echo "changes=true" >> $GITHUB_OUTPUT

      - name: Create Pull Request on significant changes
        if: steps.git-check.outputs.changes == 'true' && steps.update-wiki.outputs.files_updated == 'true'
        uses: peter-evans/create-pull-request@v5
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          commit-message: '📝 Update wiki documentation from Devin'
          title: '🤖 Automated Wiki Documentation Update'
          body: |
            ## 📝 Automated Wiki Documentation Update

            This PR contains automated updates to the wiki documentation in `.cursor/docs/`.

            ### Changes Made:
            - Updated documentation from Devin wiki
            - Timestamp: ${{ github.run_id }}

            ### Review Notes:
            - Please review the changes for accuracy
            - The documentation has been automatically formatted
            - Manual review recommended before merging

            ---
            *This PR was automatically generated by the wiki update workflow*
          branch: feature/automated-wiki-update
          base: main
          delete-branch: true

内部で使ってるscript

const fs = require('fs')
const path = require('path')
const axios = require('axios')

const REPO_NAME = process.env.GITHUB_REPOSITORY
const DOCS_DIR = '.cursor/rules'
const DEVIN_API_KEY = process.env.DEVIN_API_KEY
const DEVIN_MCP_URL = 'https://mcp.devin.ai'
const MCP_PROTOCOL_VERSION = '2025-06-18'

let mcpSessionId = null

// 動的に取得されるページリスト
let WIKI_PAGES = []

// HTTPクライアントの設定
const httpClient = axios.create({
  baseURL: DEVIN_MCP_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json, text/event-stream',
    'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
    Authorization: `Bearer ${DEVIN_API_KEY}`,
    'User-Agent': 'GitHub-Actions-Wiki-Updater/2.0',
  },
})

async function initializeMcpSession() {
  try {
    console.log('🔄 Initializing MCP session...')

    const response = await httpClient.post('/mcp', {
      jsonrpc: '2.0',
      id: 0,
      method: 'initialize',
      params: {
        protocolVersion: MCP_PROTOCOL_VERSION,
        capabilities: {
          tools: {},
        },
        clientInfo: {
          name: 'GitHub-Actions-Wiki-Updater',
          version: '2.0',
        },
      },
    })

    mcpSessionId = response.headers['mcp-session-id']
    if (mcpSessionId) {
      console.log('✅ MCP session initialized with ID:', mcpSessionId.substring(0, 8) + '...')

      // HTTPクライアントにセッションIDを追加
      httpClient.defaults.headers['Mcp-Session-Id'] = mcpSessionId
    }

    return response.data?.result
  } catch (error) {
    console.error('❌ Error initializing MCP session:', error.message)
    throw error
  }
}

function parseSSEResponse(sseData) {
  const lines = sseData.split('\n')

  for (const line of lines) {
    if (line.startsWith('data: ') && line !== 'data: ping') {
      const dataContent = line.substring(6)
      try {
        return JSON.parse(dataContent)
      } catch (e) {
        console.log('Failed to parse JSON from SSE:', e.message)
      }
    }
  }

  return null
}

// Devin MCP サーバーからwikiの構造を取得
async function fetchWikiStructure() {
  try {
    console.log('🔄 Fetching wiki structure from Devin MCP...')

    const response = await httpClient.post('/mcp', {
      jsonrpc: '2.0',
      id: 1,
      method: 'tools/call',
      params: {
        name: 'read_wiki_structure',
        arguments: {
          repoName: REPO_NAME,
        },
      },
    })

    const parsedResponse = parseSSEResponse(response.data)
    const structure = parsedResponse?.result?.content || parsedResponse?.result || response.data
    console.log('✅ Wiki structure fetched successfully')

    return structure
  } catch (error) {
    console.error('❌ Error fetching wiki structure:', error.message)
    throw error
  }
}

// Devin MCP サーバーからwikiのコンテンツを取得
async function fetchWikiContent() {
  try {
    console.log('🔄 Fetching wiki content from Devin MCP...')

    const response = await httpClient.post('/mcp', {
      jsonrpc: '2.0',
      id: 2,
      method: 'tools/call',
      params: {
        name: 'read_wiki_contents',
        arguments: {
          repoName: REPO_NAME,
        },
      },
    })

    const parsedResponse = parseSSEResponse(response.data)
    const content = parsedResponse?.result?.content || parsedResponse?.result || response.data
    console.log('✅ Wiki content fetched successfully')

    return content
  } catch (error) {
    console.error('❌ Error fetching wiki content:', error.message)
    throw error
  }
}

// wikiの構造からページリストを生成
function generatePageList(structure) {
  console.log('🔍 Processing wiki structure to generate dynamic page list')
  
  let pages = []
  let wikiText = ''
  
  if (Array.isArray(structure)) {
    for (const item of structure) {
      if (typeof item === 'object' && item.text) {
        wikiText += item.text + '\n'
      } else if (typeof item === 'string') {
        wikiText += item + '\n'
      }
    }
  } else if (typeof structure === 'object' && structure !== null && structure.text) {
    wikiText = structure.text
  } else if (typeof structure === 'string') {
    wikiText = structure
  }
  
  
  if (wikiText.trim()) {
    const lines = wikiText.split('\n')
    let pageIndex = 1
    
    for (const line of lines) {
      const trimmedLine = line.trim()
      
      const mainSectionMatch = trimmedLine.match(/^-\s*(\d+)\s+(.+)$/)
      if (mainSectionMatch) {
        const title = mainSectionMatch[2].trim()
        pages.push({
          title: title,
          id: generatePageId(title),
          filename: `${String(pageIndex).padStart(2, '0')}-${generatePageId(title)}.md`
        })
        pageIndex++
        continue
      }
      
      const subSectionMatch = trimmedLine.match(/^-\s*\d+\.\d+\s+(.+)$/)
      if (subSectionMatch) {
        const title = subSectionMatch[1].trim()
        pages.push({
          title: title,
          id: generatePageId(title),
          filename: `${String(pageIndex).padStart(2, '0')}-${generatePageId(title)}.md`
        })
        pageIndex++
        continue
      }
      
      const simpleMatch = trimmedLine.match(/^[-*]\s*(.+)$/)
      if (simpleMatch && !trimmedLine.includes(':')) {
        const title = simpleMatch[1].trim()
        if (title && !title.match(/^\d+(\.\d+)*$/)) { // 数字のみの行は除外
          pages.push({
            title: title,
            id: generatePageId(title),
            filename: `${String(pageIndex).padStart(2, '0')}-${generatePageId(title)}.md`
          })
          pageIndex++
        }
      }
    }
  }
  
  if (pages.length === 0) {
    console.log('⚠️  No pages found in wiki structure - no files will be generated')
  }
  
  console.log(`📋 Generated ${pages.length} pages from wiki structure:`)
  pages.forEach(page => {
    console.log(`  - ${page.title} (${page.filename})`)
  })
  
  return pages.map((page, index) => ({
    id: page.id,
    filename: page.filename,
    title: page.title,
    originalIndex: index,
  }))
}

// タイトルからページIDを生成
function generatePageId(title) {
  return title
    .toLowerCase()
    .replace(/[^a-z0-9\s-]/g, '')
    .replace(/\s+/g, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '')
}

// wikiのコンテンツを解析してページごとに分割
function parseWikiContent(content) {
  const pages = {}

  if (Array.isArray(content)) {
    for (const item of content) {
      const wikiText = item?.text || item || ''

      const pageSections = wikiText.split(/# Page: /g)

      for (const section of pageSections) {
        if (!section.trim()) continue

        const lines = section.split('\n')
        const pageTitle = lines[0]?.trim()
        const content = lines.slice(1).join('\n').trim()

        if (pageTitle) {
          const pageId = findPageIdByTitle(pageTitle)

          if (pageId) {
            pages[pageId] = {
              title: pageTitle,
              content: content,
              lastModified: new Date().toISOString(),
            }
          }
        }
      }
    }
  } else if (typeof content === 'string') {
    const sections = content.split(/^# /m)

    for (const section of sections) {
      if (!section.trim()) continue

      const lines = section.split('\n')
      const title = lines[0].trim()
      const body = lines.slice(1).join('\n').trim()

      const pageId = findPageIdByTitle(title)
      if (pageId) {
        pages[pageId] = {
          title: title,
          content: `# ${title}\n\n${body}`,
          lastModified: new Date().toISOString(),
        }
      }
    }
  } else if (typeof content === 'object') {
    Object.keys(content).forEach((key) => {
      const pageData = content[key]
      const pageId = findPageIdByTitle(key) || generatePageId(key)

      pages[pageId] = {
        title: pageData.title || key,
        content: pageData.content || pageData,
        lastModified: new Date().toISOString(),
      }
    })
  }

  return pages
}

// タイトルからページIDを見つける
function findPageIdByTitle(title) {
  const normalizedTitle = title.toLowerCase()

  for (const page of WIKI_PAGES) {
    if (page.title.toLowerCase() === normalizedTitle) {
      return page.id
    }
  }

  for (const page of WIKI_PAGES) {
    const pageTitle = page.title.toLowerCase()
    if (normalizedTitle.includes(pageTitle) && pageTitle.length > 3) {
      return page.id
    }
  }

  return generatePageId(title)
}

// ローカルファイルを書き込み(常に上書き)
async function writeAllFiles(wikiContent) {
  for (const page of WIKI_PAGES) {
    const filePath = path.join(DOCS_DIR, page.filename)

    try {
      // 新しいコンテンツを取得(プレースホルダーは使用しない)
      let newContent = ''
      if (wikiContent[page.id]) {
        newContent = wikiContent[page.id].content || wikiContent[page.id]
      }

      // コンテンツが空の場合はスキップ
      if (!newContent) {
        console.log(`⏭️  Skipping empty content: ${page.filename}`)
        continue
      }

      fs.writeFileSync(filePath, newContent, 'utf8')
      console.log(`✅ Written: ${page.filename}`)
    } catch (error) {
      console.error(`❌ Error writing ${page.filename}:`, error.message)
    }
  }
}



// README.mdの更新
async function updateReadme() {
  const readmePath = path.join(DOCS_DIR, 'README.md')
  const timestamp = new Date().toISOString()
  
  // Extract repository name for title generation
  const repoName = REPO_NAME.split('/')[1] || 'Unknown'
  const projectTitle = repoName.split('-').map(word => 
    word.charAt(0).toUpperCase() + word.slice(1)
  ).join(' ')

  const readmeContent = `# ${projectTitle} Documentation

This directory contains automatically updated documentation for the ${repoName} project.

## 📚 Available Documentation

${WIKI_PAGES.map((page) => `- [${page.title}](${page.filename})`).join('\n')}

## 🔄 Update Information

- **Last Updated**: ${timestamp}
- **Total Pages**: ${WIKI_PAGES.length}

## 🤖 Automation

This documentation is automatically updated daily via GitHub Actions.
The source content is synchronized from the Devin wiki using MCP tools.

### Automated Update Schedule

- **Daily**: Every day at 9:00 AM JST (00:00 UTC)
- **Manual**: Can be triggered manually through GitHub Actions

### Update Process

1. Delete existing documentation directory
2. Fetch latest content from Devin wiki
3. Overwrite with fresh content
4. Create PR if git diff shows changes

## 🛠️ Manual Update

To manually update the documentation:

\`\`\`bash
# Via MCP (if available)
node .github/scripts/update-wiki-docs-mcp.js

# Via API fallback
node .github/scripts/update-wiki-docs.js
\`\`\`

## 🔧 Configuration

The automated update system is configured through:

- **Workflow**: \`.github/workflows/update-wiki-docs.yml\`
- **MCP Script**: \`.github/scripts/update-wiki-docs-mcp.js\`
- **API Fallback**: \`.github/scripts/update-wiki-docs.js\`

### Environment Variables

- \`GITHUB_TOKEN\`: Required for automated commits
- \`FORCE_UPDATE\`: Optional, forces update even if no changes detected

## 📋 Features

- ✅ Daily automated updates
- ✅ Manual trigger support
- ✅ Simple overwrite approach
- ✅ Automated commit and PR creation
- ✅ Multiple data sources (MCP + API fallback)
- ✅ Error handling and logging
- ✅ Automatic README generation

## 🔍 Troubleshooting

### Common Issues

1. **MCP Connection Failed**: The system will fallback to API calls
2. **API Rate Limits**: Updates may be delayed during high usage
3. **Permission Errors**: Ensure \`GITHUB_TOKEN\` has proper permissions

### Logs

Check GitHub Actions logs for detailed information about update processes:
- Go to Actions tab in the repository
- Select "Update Wiki Documentation" workflow
- Review the latest run for detailed logs

## 🚀 Getting Started

1. The documentation is automatically maintained
2. No manual intervention required for regular updates
3. Review automatically created PRs for significant changes
4. Use the documentation as a reference for project development

## 🤝 Contributing

While this documentation is automatically generated, you can:

1. **Report Issues**: If you notice outdated or incorrect information
2. **Suggest Improvements**: For the automation scripts or documentation structure
3. **Manual Updates**: For urgent corrections (will be overwritten on next auto-update)

## 🔗 Links

- [Devin Wiki](https://app.devin.ai/wiki/${REPO_NAME})
- [GitHub Actions Workflow](.github/workflows/update-wiki-docs.yml)
- [Project Repository](https://github.com/${REPO_NAME})

---

*This documentation is automatically generated and updated.*
`

  fs.writeFileSync(readmePath, readmeContent, 'utf8')
  console.log('✅ Updated README.md')
}

// メイン実行関数
async function main() {
  try {
    console.log('🚀 Starting Devin MCP-based wiki documentation update...')
    console.log(`📂 Repository: ${REPO_NAME}`)

    // 必要な環境変数をチェック
    if (!DEVIN_API_KEY) {
      throw new Error('DEVIN_API_KEY environment variable is required')
    }

    if (fs.existsSync(DOCS_DIR)) {
      fs.rmSync(DOCS_DIR, { recursive: true, force: true })
      console.log(`🗑️  Deleted existing directory: ${DOCS_DIR}`)
    }
    fs.mkdirSync(DOCS_DIR, { recursive: true })
    console.log(`📁 Created directory: ${DOCS_DIR}`)

    await initializeMcpSession()

    // wikiの構造を取得
    const wikiStructure = await fetchWikiStructure()

    // ページリストを生成
    WIKI_PAGES = generatePageList(wikiStructure)
    console.log(`📋 Generated ${WIKI_PAGES.length} pages from wiki structure`)

    // wikiの内容を取得
    const rawWikiContent = await fetchWikiContent()

    // wikiの内容を解析
    const wikiContent = parseWikiContent(rawWikiContent)

    // ローカルファイルを更新(常に上書き)
    await writeAllFiles(wikiContent)

    // READMEを更新
    await updateReadme()

    console.log('✅ Devin MCP-based wiki documentation update completed!')
    console.log('📝 All files have been overwritten with fresh wiki content')
  } catch (error) {
    console.error('❌ Fatal error:', error)
    console.error('Stack trace:', error.stack)

    // 詳細なエラー情報を出力
    if (error.response) {
      console.error('Response data:', error.response.data)
      console.error('Response status:', error.response.status)
    }

    process.exit(1)
  }
}

// 実行
main()

CLAUDE.md

以下を書いてるだけ

詳細な情報については@.cursor/rules/README.mdを参照してください。

GitHub Claude Code Action 導入 with Claude 4 Sonnet on Amazon Bedrock



ひさびさに書くよ
参考:
docs.anthropic.com

AWS

CloudFormation作るまでもないかな

IAMポリシーの作成

https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/policies

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor",
            "Effect": "Allow",
            "Action": [
                "bedrock:InvokeModel",
                "bedrock:InvokeModelWithResponseStream"
            ],
            "Resource": [
                "arn:aws:bedrock:*::foundation-model/*",
                "arn:aws:bedrock:*:*:inference-profile/*"
            ]
        }
    ]
}

IAMロールの作成

https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/roles

  1. 信頼されたエンティティ: ウェブアイデンティティ
  2. アイデンティティプロバイダー: 上で作ったOIDCプロパイダー
  3. 許可ポリシー: 上で作ったポリシー

ロールのARNは後で使うので控えておきましょう

Bedrock

https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/modelaccess

「Claude Sonnet 4」にチェックを入れて、次へ
会社名やらなんやら必要な情報をいれましょう
即付与されてたよ

https://us-east-1.console.aws.amazon.com/bedrock/home?region=us-east-1#/inference-profiles
Claude Sonnet 4のInference profile ARNを控えておく

Github

Actions Secretの設定

OrganizationでもRepositoryでもお好きな方に。うちはOrganizationに指定してる。

  1. CLAUDE_CODE_ACTION_AWS_ASSUME_ROLE: IAMロールのARN
  2. CLAUDE_CODE_ACTION_AWS_BEDROCK_MODEL: Claude Sonnet 4のInference profile ARN
  3. ORG_GITHUB_TOKEN: GithubのPAT

Actions

使いたいリポジトリに.github/workflows/claude-code-action.ymlを追加しましょう

name: Claude Code Action

on:
  issues:
    types: [opened]
  issue_comment:
    types: [created]
  pull_request_review:
    types: [submitted]
  pull_request_review_comment:
    types: [created]

jobs:
  claude-code-action:
    permissions:
      id-token: write
      issues: write
      pull-requests: write
      contents: write

    runs-on: ubuntu-latest

    if: |
      (github.event_name == 'issues' && github.event.issue && contains(github.event.issue.body, '@claude')) ||
      (github.event_name == 'issue_comment' && github.event.comment && contains(github.event.comment.body, '@claude')) ||
      (github.event_name == 'pull_request_review' && github.event.review && contains(github.event.review.body, '@claude')) ||
      (github.event_name == 'pull_request_review_comment' && github.event.comment && contains(github.event.comment.body, '@claude'))

    env:
      AWS_REGION: us-east-1
      TZ: Asia/Tokyo
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.CLAUDE_CODE_ACTION_AWS_ASSUME_ROLE }}
          aws-region: us-east-1

      - name: Run Claude Code Action
        id: claude-code
        uses: anthropics/claude-code-action@beta
        with:
          model: ${{ secrets.CLAUDE_CODE_ACTION_AWS_BEDROCK_MODEL }}
          use_bedrock: 'true'
          github_token: ${{ secrets.ORG_GITHUB_TOKEN }}
          trigger_phrase: '@claude'
          timeout_minutes: '60'
          allowed_tools: 'Bash'

あとはissueなり、PRなりで`@claude`と入れれば動いてくれる



EC2でdocker起動

今日は仕事しないつもりなんですけどね。はい。

てきとーにさくっとEC2でRails + MySQL + phpMyAdminのdocker-compose起動したい時のメモ
まぁコンテナが使うポートをセキュリティグループで許可する以外どんな構成でもえーと思うで。

おもむろにEC2のインスタンスを起動しましょう。
パブリックサブネットにパブリックIP自動でセキュリティグループは22, 80, 8080のインバウンドを開けておきましょう。
で、ユーザーデータにこれをコピペ
よく使いそうなもんもてきとーにインストールしておきましょ。

#!/bin/bash
sudo yum update -y
sudo yum install -y git docker make vim curl
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -a -G docker ec2-user
sudo mkdir -p /usr/local/lib/docker/cli-plugins
VER=2.39.1
sudo curl \
-L https://github.com/docker/compose/releases/download/v${VER}/docker-compose-$(uname -s)-$(uname -m) \
-o /usr/local/lib/docker/cli-plugins/docker-compose
sudo chmod +x /usr/local/lib/docker/cli-plugins/docker-compose
sudo ln -s /usr/local/lib/docker/cli-plugins/docker-compose /usr/bin/docker-compose

起動したらsshで入って、git cloneなり、docker-compose buildなり、docker-compose upなり、ご自由に。

// -------------

宣伝

最近はPoC開発が好きです。
もちろん大規模・高トラフィックも安定に自信ありますけども。
お仕事ください。

最近よく使うものたち。


Flutter - iOS / Android向け入門3 - json_annotation編

第3回です。

オブジェクト > jsonやらjson > オブジェクトやらしたいよね?
いい感じのORMがなくて、一旦無難にこれ。

json_annotation | Dart Package

おもむろにこんな定義を用意しましょう。

import 'package:json_annotation/json_annotation.dart';

part 'sample.g.dart';

@JsonSerializable()
class Sample {
  final String hoge;
  const Sample(this.hoge);

  factory Sample.fromJson(Map<String, dynamic> json) =>
      _$SampleFromJson(json);
  Map<String, dynamic> toJson() => _$SampleToJson(this);
}

そして実行

`flutter pub run build_runner build`

profile.g.dartっていうのができるよ。
あとは普通にjsonEncodeだったり、jsonDecodeだったりに使うだけ。


// -------------

宣伝

最近はPoC開発が好きです。
もちろん大規模・高トラフィックも安定に自信ありますけども。
お仕事ください。

最近よく使うものたち。