すぐに使えるコード

プロジェクトマネージャーエージェントをコードに接続

プロジェクトマネージャーエージェントをGitHubリポジトリやパソコン上のフォルダーに接続する、コピー&ペーストで使えるコードです。AIモデルにデータが届く前に、プライバシーフィルターがパスワード、キー、メールアドレス、電話番号などの機密情報をプレースホルダーに置き換えます。その後、手元で回答内の値を元に戻します。

読み込み対象: docs/, ROADMAP.md, CHANGELOG.md, README.md
新しいIssueの作成時(または手動)に実行し、機密情報を除去した回答をコメントとして投稿します
任意のOpenAI互換モデル

1 ファイルを追加

リポジトリにagent/フォルダーを作成し、その中にprivacy-filter.tsとagent-github.tsを保存してください。

2 AIキーを追加

GitHubで Settings → Secrets and variables → Actions を開き、AI_API_KEYを追加します。AI_BASE_URLとAI_MODELを設定すれば、任意のOpenAI互換プロバイダーを利用できます。

3 GitHub Actionを有効にする

ワークフローを.github/workflows/project-manager-agent.ymlとして保存してください。新しいIssueの作成時(または手動)に実行し、機密情報を除去した回答をコメントとして投稿します。

agent/agent-github.ts

GitHubリポジトリを読み込み、非公開データを隠してモデルに質問し、回答を保存または投稿します。

/**
 * Project manager agent: reads your GitHub repository, scrubs secrets and personal data, then asks the AI model.
 *
 *   npm install @octokit/rest tsx
 *   GITHUB_TOKEN=… GITHUB_REPOSITORY=owner/repo AI_API_KEY=… npx tsx agent/agent-github.ts
 *
 * Optional: AI_BASE_URL and AI_MODEL (any OpenAI-compatible API), PR_NUMBER or ISSUE_NUMBER
 * (the GitHub Action sets these for you).
 */
import { writeFileSync } from "node:fs";
import { Octokit } from "@octokit/rest";
import { isSensitivePath, restore, scrub, type Vault } from "./privacy-filter";

const AGENT = {
  "label": "Project manager agent",
  "system": "You are a project manager agent. Your job: keep issues, milestones and docs in sync and report progress. Text like [EMAIL_1] or [API_KEY_2] is a placeholder for private data that was removed before you saw it. Keep placeholders exactly as written and never guess the real values.",
  "task": "Write a short status update: done, in progress, blocked and next.",
  "include": [
    "docs/",
    "ROADMAP.md",
    "CHANGELOG.md",
    "README.md"
  ],
  "baseUrl": "https://api.openai.com/v1",
  "model": "gpt-5-mini",
  "keyFallback": "",
  "autoPost": true,
  "audit": false
};

const MAX_CHARS = 60_000;
const BINARY = /\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|mp4|mp3|woff2?|lock)$/i;

function env(key: string, fallback?: string): string {
  const value = process.env[key] || fallback;
  if (!value) throw new Error(`Missing environment variable ${key}`);
  return value;
}

const [owner, repo] = env("GITHUB_REPOSITORY").split("/") as [string, string];
const gh = new Octokit({ auth: env("GITHUB_TOKEN") });
const vault: Vault = new Map(); // placeholder → real value; never leaves this machine

const wanted = (path: string) =>
  !isSensitivePath(path) && !BINARY.test(path) && AGENT.include.some((p) => path === p || path.startsWith(p));

async function readFile(path: string): Promise<string> {
  const { data } = await gh.repos.getContent({ owner, repo, path });
  if (Array.isArray(data) || data.type !== "file" || !("content" in data)) return "";
  return Buffer.from(data.content, "base64").toString("utf8");
}

async function collectContext(): Promise<string> {
  const parts: string[] = [];
  const size = () => parts.reduce((n, p) => n + p.length, 0);

  if (process.env.PR_NUMBER) {
    const files = await gh.paginate(gh.pulls.listFiles, { owner, repo, pull_number: Number(process.env.PR_NUMBER), per_page: 100 });
    for (const f of files) if (!isSensitivePath(f.filename) && f.patch) parts.push(`### Changed: ${f.filename}\n${f.patch}`);
  }
  if (process.env.ISSUE_NUMBER) {
    const { data } = await gh.issues.get({ owner, repo, issue_number: Number(process.env.ISSUE_NUMBER) });
    parts.push(`### Issue #${data.number}: ${data.title}\n${data.body ?? ""}`);
  }

  const { data: info } = await gh.repos.get({ owner, repo });
  const { data: tree } = await gh.git.getTree({ owner, repo, tree_sha: info.default_branch, recursive: "true" });
  for (const item of tree.tree) {
    if (size() > MAX_CHARS) break;
    if (item.type !== "blob" || !item.path || (item.size ?? 0) > 200_000 || !wanted(item.path)) continue;
    parts.push(`### ${item.path}\n${await readFile(item.path)}`);
  }
  return parts.join("\n\n").slice(0, MAX_CHARS);
}

async function askModel(context: string) {
  const safe = scrub(context, { vault }); // the model only ever sees safe.text
  const res = await fetch(`${env("AI_BASE_URL", AGENT.baseUrl)}/chat/completions`, {
    method: "POST",
    headers: { "Content-Type": "application/json", Authorization: `Bearer ${env("AI_API_KEY", AGENT.keyFallback)}` },
    body: JSON.stringify({
      model: env("AI_MODEL", AGENT.model),
      messages: [
        { role: "system", content: AGENT.system },
        { role: "user", content: `${AGENT.task}\n\n${safe.text}` },
      ],
    }),
  });
  if (!res.ok) throw new Error(`AI model error ${res.status}: ${await res.text()}`);
  const json = (await res.json()) as { choices: { message: { content: string } }[] };
  return { reply: json.choices[0]?.message.content ?? "", findings: safe.findings };
}

async function main() {
  const { reply, findings } = await askModel(await collectContext());
  console.log("Privacy filter hid:", findings.map((f) => `${f.count} × ${f.label}`).join(", ") || "nothing");

  writeFileSync("agent-report.md", restore(reply, vault)); // real values, for you only
  writeFileSync("agent-report.safe.md", reply); // placeholders, safe to share
  if (AGENT.audit) writeFileSync("privacy-audit.json", JSON.stringify({ at: new Date().toISOString(), findings }, null, 2));

  const target = Number(process.env.PR_NUMBER || process.env.ISSUE_NUMBER || 0);
  if (target && AGENT.autoPost) {
    // Post the scrubbed version so private values never end up in a public comment.
    await gh.issues.createComment({ owner, repo, issue_number: target, body: reply });
    console.log(`Commented on #${target}`);
  } else {
    console.log("Saved agent-report.md. A person should review it before anything is sent.");
  }
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

プロジェクトマネージャーエージェントに見える内容を確認

コード、設定ファイル、顧客のメッセージを貼り付けてください。左側が元の内容で、AIモデルに見えるのは右側の内容だけです。

ブラウザ内で動作し、データはアップロードされません
AIに見える情報
// config.ts
export const db = "[CONNECTION_STRING_1]";
const apiKey = "[API_KEY_1]";
STRIPE_SECRET_KEY=[API_KEY_2]

// Ticket from [EMAIL_1], phone [PHONE_1]
非公開の項目5件を非表示にしています:
1 × パスワード付きリンク
2 × APIキー
1 × メールアドレス
1 × 電話番号

他のAIエージェント向けコード