From 0b29a635d30dcf147755eaa9c5bd852429bc4bc9 Mon Sep 17 00:00:00 2001 From: e-mon Date: Fri, 13 Mar 2026 13:32:32 +0900 Subject: [PATCH 1/4] feat(gog): add recurrence, attendees, and other calendar params Expose missing gog CLI flags for calendar create/update MCP tools: - rrule: recurring event rules (weekly, monthly, etc.) - attendees/addAttendee: participant management - allDay: all-day event support - reminder: custom reminders (popup/email) - visibility, transparency: event visibility controls - scope, originalStart: recurring event instance targeting (update only) Co-Authored-By: Claude Opus 4.6 --- src/mcp/gog-tools.ts | 56 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/src/mcp/gog-tools.ts b/src/mcp/gog-tools.ts index f46a4e5..4b27370 100644 --- a/src/mcp/gog-tools.ts +++ b/src/mcp/gog-tools.ts @@ -179,7 +179,7 @@ export const GOG_TOOLS: GogToolDef[] = [ }, { name: 'gog_calendar_create', - description: 'Create a calendar event.', + description: 'Create a calendar event. Supports recurring events via rrule.', inputSchema: { type: 'object', properties: { @@ -187,9 +187,23 @@ export const GOG_TOOLS: GogToolDef[] = [ summary: { type: 'string', description: 'Event title' }, from: { type: 'string', description: 'Start date/time (ISO 8601)' }, to: { type: 'string', description: 'End date/time (ISO 8601)' }, + allDay: { type: 'boolean', description: 'All-day event (use date-only in from/to)' }, + rrule: { + type: 'string', + description: + 'Recurrence rule (e.g. "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR", ' + + '"RRULE:FREQ=MONTHLY;BYMONTHDAY=11"). Multiple rules comma-separated.', + }, + attendees: { type: 'string', description: 'Comma-separated attendee emails' }, + reminder: { + type: 'string', + description: 'Custom reminders as method:duration (e.g. "popup:30m", "email:1d"). Comma-separated, max 5.', + }, eventColor: { type: 'string', description: 'Event color ID (1-11)' }, location: { type: 'string', description: 'Event location' }, description: { type: 'string', description: 'Event description' }, + visibility: { type: 'string', description: 'Event visibility: default, public, private, confidential' }, + transparency: { type: 'string', description: 'Show as busy (opaque) or free (transparent)' }, }, required: ['summary', 'from', 'to'], }, @@ -203,14 +217,20 @@ export const GOG_TOOLS: GogToolDef[] = [ str(p.from), '--to', str(p.to), + ...(p.allDay ? ['--all-day'] : []), + ...optFlag(p, 'rrule', '--rrule'), + ...optFlag(p, 'attendees', '--attendees'), + ...optFlag(p, 'reminder', '--reminder'), ...optFlag(p, 'eventColor', '--event-color'), ...optFlag(p, 'location', '--location'), ...optFlag(p, 'description', '--description'), + ...optFlag(p, 'visibility', '--visibility'), + ...optFlag(p, 'transparency', '--transparency'), ], }, { name: 'gog_calendar_update', - description: 'Update an existing calendar event.', + description: 'Update an existing calendar event. For recurring events, use scope to control which instances to update.', inputSchema: { type: 'object', properties: { @@ -219,7 +239,28 @@ export const GOG_TOOLS: GogToolDef[] = [ summary: { type: 'string', description: 'New event title' }, from: { type: 'string', description: 'New start date/time (ISO 8601)' }, to: { type: 'string', description: 'New end date/time (ISO 8601)' }, + allDay: { type: 'boolean', description: 'All-day event (use date-only in from/to)' }, + rrule: { + type: 'string', + description: + 'Recurrence rule (e.g. "RRULE:FREQ=WEEKLY;BYDAY=MO"). Set empty to clear recurrence.', + }, + attendees: { type: 'string', description: 'Comma-separated attendee emails (replaces all; set empty to clear)' }, + addAttendee: { type: 'string', description: 'Comma-separated attendee emails to add (preserves existing)' }, + reminder: { + type: 'string', + description: 'Custom reminders as method:duration (e.g. "popup:30m"). Set empty to clear.', + }, eventColor: { type: 'string', description: 'Event color ID (1-11)' }, + location: { type: 'string', description: 'Event location' }, + description: { type: 'string', description: 'Event description' }, + visibility: { type: 'string', description: 'Event visibility: default, public, private, confidential' }, + transparency: { type: 'string', description: 'Show as busy (opaque) or free (transparent)' }, + scope: { type: 'string', description: 'For recurring events: single, future, or all (default: all)' }, + originalStart: { + type: 'string', + description: 'Original start time of instance (required for scope=single or scope=future)', + }, }, required: ['eventId'], }, @@ -231,7 +272,18 @@ export const GOG_TOOLS: GogToolDef[] = [ ...optFlag(p, 'summary', '--summary'), ...optFlag(p, 'from', '--from'), ...optFlag(p, 'to', '--to'), + ...(p.allDay ? ['--all-day'] : []), + ...optFlag(p, 'rrule', '--rrule'), + ...optFlag(p, 'attendees', '--attendees'), + ...optFlag(p, 'addAttendee', '--add-attendee'), + ...optFlag(p, 'reminder', '--reminder'), ...optFlag(p, 'eventColor', '--event-color'), + ...optFlag(p, 'location', '--location'), + ...optFlag(p, 'description', '--description'), + ...optFlag(p, 'visibility', '--visibility'), + ...optFlag(p, 'transparency', '--transparency'), + ...optFlag(p, 'scope', '--scope'), + ...optFlag(p, 'originalStart', '--original-start'), ], }, From c82663ee5ff99e4a1a485cf917123eca460fea9e Mon Sep 17 00:00:00 2001 From: e-mon Date: Fri, 13 Mar 2026 17:04:52 +0900 Subject: [PATCH 2/4] style(gog): fix biome formatting Co-Authored-By: Claude Opus 4.6 --- src/mcp/gog-tools.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/mcp/gog-tools.ts b/src/mcp/gog-tools.ts index 4b27370..ee2dffe 100644 --- a/src/mcp/gog-tools.ts +++ b/src/mcp/gog-tools.ts @@ -197,7 +197,8 @@ export const GOG_TOOLS: GogToolDef[] = [ attendees: { type: 'string', description: 'Comma-separated attendee emails' }, reminder: { type: 'string', - description: 'Custom reminders as method:duration (e.g. "popup:30m", "email:1d"). Comma-separated, max 5.', + description: + 'Custom reminders as method:duration (e.g. "popup:30m", "email:1d"). Comma-separated, max 5.', }, eventColor: { type: 'string', description: 'Event color ID (1-11)' }, location: { type: 'string', description: 'Event location' }, @@ -230,7 +231,8 @@ export const GOG_TOOLS: GogToolDef[] = [ }, { name: 'gog_calendar_update', - description: 'Update an existing calendar event. For recurring events, use scope to control which instances to update.', + description: + 'Update an existing calendar event. For recurring events, use scope to control which instances to update.', inputSchema: { type: 'object', properties: { @@ -242,11 +244,16 @@ export const GOG_TOOLS: GogToolDef[] = [ allDay: { type: 'boolean', description: 'All-day event (use date-only in from/to)' }, rrule: { type: 'string', - description: - 'Recurrence rule (e.g. "RRULE:FREQ=WEEKLY;BYDAY=MO"). Set empty to clear recurrence.', + description: 'Recurrence rule (e.g. "RRULE:FREQ=WEEKLY;BYDAY=MO"). Set empty to clear recurrence.', + }, + attendees: { + type: 'string', + description: 'Comma-separated attendee emails (replaces all; set empty to clear)', + }, + addAttendee: { + type: 'string', + description: 'Comma-separated attendee emails to add (preserves existing)', }, - attendees: { type: 'string', description: 'Comma-separated attendee emails (replaces all; set empty to clear)' }, - addAttendee: { type: 'string', description: 'Comma-separated attendee emails to add (preserves existing)' }, reminder: { type: 'string', description: 'Custom reminders as method:duration (e.g. "popup:30m"). Set empty to clear.', From 8e89466f8073dd414297b2a335afb9e0cebf9aed Mon Sep 17 00:00:00 2001 From: e-mon Date: Fri, 13 Mar 2026 17:08:19 +0900 Subject: [PATCH 3/4] style: convert all Japanese text to English across codebase Replace Japanese comments, UI labels, CLI messages, LLM prompts, and skill definitions with English equivalents to make the repository accessible to English-speaking contributors. Test fixtures with intentional Japanese test data are left unchanged. Co-Authored-By: Claude Opus 4.6 --- src/agent/session/daily-summary.ts | 38 ++++----- src/channels/chat-handlers.ts | 2 +- src/cli/commands/browser.ts | 18 ++--- src/cli/commands/run.ts | 2 +- src/cli/index.ts | 2 +- src/config/paths.ts | 2 +- src/cron/cli.ts | 22 +++--- src/mcp/admin-server.ts | 22 +++--- src/mcp/cron-server.ts | 38 ++++----- src/skills/cli.ts | 22 +++--- src/skills/manager.ts | 78 +++++++++---------- src/skills/scanner.ts | 58 +++++++------- src/skills/types.ts | 12 +-- src/tui/pi/chat-log.ts | 6 +- src/tui/pi/debug-overlay.ts | 8 +- src/tui/pi/interactive-app.ts | 10 +-- src/tui/pi/workspace-viewer.ts | 12 +-- .../agent-browser/references/site-patterns.md | 28 +++---- .../.gemini/skills/translate-preview/SKILL.md | 8 +- .../references/build-injection.js | 2 +- .../references/inject-translations.js | 6 +- .../translate-preview/references/render.js | 2 +- .../references/twitter-render.js | 12 +-- 23 files changed, 205 insertions(+), 205 deletions(-) diff --git a/src/agent/session/daily-summary.ts b/src/agent/session/daily-summary.ts index 810e778..ca34387 100644 --- a/src/agent/session/daily-summary.ts +++ b/src/agent/session/daily-summary.ts @@ -149,8 +149,8 @@ export async function generateHeartbeatActivityLog( `# Heartbeat Activity — ${dateStr}`, '', '## Summary', - `- ${grouped.okCount}回 HEARTBEAT_OK`, - `- ${grouped.actions.length}回 アクション実行`, + `- ${grouped.okCount}x HEARTBEAT_OK`, + `- ${grouped.actions.length}x actions executed`, '', '## Activity Log', '', @@ -255,19 +255,19 @@ export async function generateDailySummary( // Sessions section lines.push('## Sessions'); if (sessions.length === 0) { - lines.push('(セッションなし)', ''); + lines.push('(No sessions)', ''); } else { for (const s of sessions) { - lines.push(`- ${s.trigger}: ${s.title}(${s.durationMin}分, ${s.tokens}トークン)`); + lines.push(`- ${s.trigger}: ${s.title} (${s.durationMin}min, ${s.tokens} tokens)`); } lines.push(''); } // Heartbeat section lines.push('## Heartbeat'); - lines.push(`- ${heartbeat.okCount}回 HEARTBEAT_OK`); + lines.push(`- ${heartbeat.okCount}x HEARTBEAT_OK`); if (heartbeat.actions.length > 0) { - lines.push(`- ${heartbeat.actions.length}回 アクション実行:`); + lines.push(`- ${heartbeat.actions.length}x actions executed:`); for (const action of heartbeat.actions) { const time = toLocalTime(action.timestamp, timezone); const tools = [...new Set(action.toolCalls.map((tc) => tc.name))].join(', '); @@ -287,10 +287,10 @@ export async function generateDailySummary( // Cron Jobs section lines.push('## Cron Jobs'); if (cronStats.jobs.length === 0) { - lines.push('(cronジョブなし)', ''); + lines.push('(No cron jobs)', ''); } else { for (const job of cronStats.jobs) { - const status = job.errors > 0 ? `${job.runs - job.errors}/${job.runs} ✓` : `${job.runs}回実行 ✓`; + const status = job.errors > 0 ? `${job.runs - job.errors}/${job.runs} ✓` : `${job.runs}x runs ✓`; lines.push(`- ${job.jobId}: ${status}`); } lines.push(''); @@ -562,9 +562,9 @@ async function summarizeHeartbeatActions( .join('\n\n'); const prompt = [ - '以下のハートビートアクション実行ログを、各エントリごとに「### HH:MM — カテゴリ」形式で要約してください。', - '各エントリは2-3文で、何を検知し何をしたかを簡潔に記述。', - 'Markdown以外のメタ情報は出力しないでください。', + 'Summarize the following heartbeat action execution logs, with each entry in "### HH:MM — Category" format.', + 'Each entry should be 2-3 sentences, concisely describing what was detected and what action was taken.', + 'Output only Markdown content — no meta-information.', '', entriesText, ].join('\n'); @@ -594,14 +594,14 @@ async function generateHighlights( const dataParts: string[] = []; if (sessions.length > 0) { - dataParts.push('セッション:'); + dataParts.push('Sessions:'); for (const s of sessions) { - dataParts.push(`- ${s.trigger}: ${s.title}(${s.durationMin}分)`); + dataParts.push(`- ${s.trigger}: ${s.title} (${s.durationMin}min)`); } } if (heartbeat.actions.length > 0) { - dataParts.push('ハートビートアクション:'); + dataParts.push('Heartbeat Actions:'); for (const a of heartbeat.actions) { const time = a.timestamp.substring(11, 16); dataParts.push(`- ${time}: ${a.responseText.substring(0, 200)}`); @@ -609,14 +609,14 @@ async function generateHighlights( } if (cronStats.jobs.length > 0) { - dataParts.push('Cronジョブ:'); + dataParts.push('Cron Jobs:'); for (const j of cronStats.jobs) { - dataParts.push(`- ${j.jobId}: ${j.runs}回実行`); + dataParts.push(`- ${j.jobId}: ${j.runs}x runs`); } } if (dailyLogContent) { - dataParts.push('エージェントメモ(Daily Log):'); + dataParts.push('Agent Notes (Daily Log):'); dataParts.push(dailyLogContent.substring(0, 2000)); } @@ -624,8 +624,8 @@ async function generateHighlights( if (dataParts.length === 0) return ''; const prompt = [ - '以下の1日のアクティビティデータから、1-3文の簡潔な日本語ハイライトを生成してください。', - '重要なイベントや成果を中心に。Markdown不要、プレーンテキストのみ。', + 'From the following daily activity data, generate a concise highlight in 1-3 sentences.', + 'Focus on important events and accomplishments. Plain text only — no Markdown.', '', dataParts.join('\n'), ].join('\n'); diff --git a/src/channels/chat-handlers.ts b/src/channels/chat-handlers.ts index 8e912ed..ac81208 100644 --- a/src/channels/chat-handlers.ts +++ b/src/channels/chat-handlers.ts @@ -260,7 +260,7 @@ async function buildEventData(thread: Thread, message: Message): Promise { if (!existsSync(BROWSER_STATE_PATH)) { - process.stderr.write('認証状態ファイルが存在しません。\n'); + process.stderr.write('Auth state file does not exist.\n'); return; } rmSync(BROWSER_STATE_PATH, { force: true }); - process.stderr.write(`削除しました: ${BROWSER_STATE_PATH}\n`); + process.stderr.write(`Deleted: ${BROWSER_STATE_PATH}\n`); }); } diff --git a/src/cli/commands/run.ts b/src/cli/commands/run.ts index dc0d0a9..47eb321 100644 --- a/src/cli/commands/run.ts +++ b/src/cli/commands/run.ts @@ -128,7 +128,7 @@ export function registerRunCommand(program: Command): void { const useInteractive = useTui && !prompt; if (useInteractive && !config.setupCompleted) { - process.stderr.write('初回セットアップが未完了です。セットアップを開始します...\n'); + process.stderr.write('Initial setup is not complete. Starting setup...\n'); const { runSetupWizard } = await import('./setup.js'); await runSetupWizard(config, workspacePath); } diff --git a/src/cli/index.ts b/src/cli/index.ts index ea6d947..ba8f59f 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -62,7 +62,7 @@ program.addCommand(buildSkillCommand()); // Show help when no command is given (default Commander behavior is silent exit) if (process.argv.length <= 2) { program.outputHelp(); - process.stderr.write('\n はじめての方は geminiclaw setup を実行してください。\n\n'); + process.stderr.write('\n New here? Run geminiclaw setup to get started.\n\n'); } else { program.parse(); } diff --git a/src/config/paths.ts b/src/config/paths.ts index a775145..cf6769b 100644 --- a/src/config/paths.ts +++ b/src/config/paths.ts @@ -10,7 +10,7 @@ import type { Config } from './schema.js'; export const GEMINICLAW_HOME = join(homedir(), '.geminiclaw'); export const CONFIG_PATH = join(GEMINICLAW_HOME, 'config.json'); -/** GeminiClaw固有のGemini CLI設定ファイル。~/.gemini/settings.jsonには書かない。 */ +/** GeminiClaw-specific Gemini CLI settings file. Never written to ~/.gemini/settings.json. */ export const GEMINICLAW_SETTINGS_PATH = join(GEMINICLAW_HOME, 'settings.json'); export const BROWSER_PROFILE_DIR = join(GEMINICLAW_HOME, 'browser-profile'); diff --git a/src/cron/cli.ts b/src/cron/cli.ts index f275c46..ce12b4f 100644 --- a/src/cron/cli.ts +++ b/src/cron/cli.ts @@ -1,8 +1,8 @@ /** - * cron/cli.ts — `geminiclaw cron` サブコマンド群。 + * cron/cli.ts — `geminiclaw cron` subcommands. * - * cron list / add / rm を提供する。 - * jobs.json はソースオブトゥルースなので CLI は薄いラッパーに徹する。 + * Provides cron list / add / rm commands. + * jobs.json is the source of truth; the CLI is a thin wrapper around the store. */ import { randomUUID } from 'node:crypto'; @@ -13,12 +13,12 @@ import { addJob, computeInitialNextRun, editJob, listJobs, loadRunLog, removeJob import type { CronJob } from './types.js'; /** - * スケジュール文字列をパースする。 + * Parse a schedule string into a schedule object. * - * 対応フォーマット: - * - "every 30m" / "every 2h" → EverySchedule - * - "at 2026-03-01T09:00:00" → AtSchedule - * - "0 9 * * *" → CronSchedule (5 フィールド) + * Supported formats: + * - "every 30m" / "every 2h" -> EverySchedule + * - "at 2026-03-01T09:00:00" -> AtSchedule + * - "0 9 * * *" -> CronSchedule (5 fields) */ function parseSchedule(input: string): CronJob['schedule'] { const trimmed = input.trim(); @@ -68,7 +68,7 @@ function formatSchedule(job: CronJob): string { } /** - * `geminiclaw cron` コマンドツリーを構築して返す。 + * Build and return the `geminiclaw cron` command tree. */ export function buildCronCommand(): Command { const cron = new Command('cron').description('Cron job management'); @@ -99,7 +99,7 @@ export function buildCronCommand(): Command { const status = j.enabled ? 'enabled ' : 'disabled'; const tz = j.timezone || config.timezone || undefined; const next = j.nextRunAt - ? new Date(j.nextRunAt).toLocaleString('ja-JP', tz ? { timeZone: tz } : undefined) + ? new Date(j.nextRunAt).toLocaleString('en-US', tz ? { timeZone: tz } : undefined) : '—'; const sched = formatSchedule(j); process.stdout.write( @@ -336,7 +336,7 @@ export function buildCronCommand(): Command { const tz = config.timezone || undefined; process.stdout.write(`Run history for ${id} (last ${entries.length}):\n\n`); for (const e of entries) { - const time = new Date(e.timestamp).toLocaleString('ja-JP', tz ? { timeZone: tz } : undefined); + const time = new Date(e.timestamp).toLocaleString('en-US', tz ? { timeZone: tz } : undefined); const reason = e.reason ? ` — ${e.reason}` : ''; process.stdout.write(` ${time} ${e.status}${reason}\n`); } diff --git a/src/mcp/admin-server.ts b/src/mcp/admin-server.ts index 1023de8..01ef676 100644 --- a/src/mcp/admin-server.ts +++ b/src/mcp/admin-server.ts @@ -156,14 +156,14 @@ const TOOLS = [ ]; /** - * skill install を直接ハンドルする。 + * Handle skill install directly (not via CLI subprocess). * - * フロー: - * 1. staging dir に bunx skills add(無害) - * 2. セキュリティスキャン - * 3. safe → 即座に workspace に移動 - * 4. warning/danger → findings 付きで ask_user → ユーザー判断 - * 5. staging クリーンアップ + * Flow: + * 1. bunx skills add into a staging dir (harmless) + * 2. Run security scan + * 3. safe -> move to workspace immediately + * 4. warning/danger -> show findings via ask_user -> user decides + * 5. Clean up staging dir */ async function handleSkillInstall( workspace: string, @@ -172,7 +172,7 @@ async function handleSkillInstall( const { installSkill, confirmInstall, cleanupStaging } = await import('../skills/manager.js'); const start = Date.now(); - // args をパース: [--skill ] [--force] + // Parse args: [--skill ] [--force] const ref = args.find((a) => !a.startsWith('-')); if (!ref) { return { content: [{ type: 'text', text: 'Error: missing skill source reference' }], isError: true }; @@ -190,7 +190,7 @@ async function handleSkillInstall( const output: string[] = []; - // safe スキル: supervised モードではユーザー確認を挟む + // Safe skills: require user confirmation in supervised mode if (result.scanned.length > 0) { const names = result.scanned.join(', '); await confirmIfNeeded(workspace, 'write', `Install safe skills: ${names}`); @@ -199,7 +199,7 @@ async function handleSkillInstall( } } - // warned スキル: findings 付きで ask_user + // Warned skills: show findings via ask_user if (result.warned.length > 0 && result._stagingDir) { for (const name of result.warned) { const report = result.reports[name]; @@ -222,7 +222,7 @@ async function handleSkillInstall( } } - // blocked スキル + // Blocked skills for (const name of result.blocked) { const report = result.reports[name]; const findingsText = report diff --git a/src/mcp/cron-server.ts b/src/mcp/cron-server.ts index 3541c77..4862a61 100644 --- a/src/mcp/cron-server.ts +++ b/src/mcp/cron-server.ts @@ -136,9 +136,9 @@ const TOOLS = [ function formatSchedule(schedule: CronJob['schedule']): string { switch (schedule.type) { case 'at': - return `一回限り: ${schedule.datetime}`; + return `once: ${schedule.datetime}`; case 'every': - return `${schedule.intervalMin}分ごと`; + return `every ${schedule.intervalMin}min`; case 'cron': return `cron: ${schedule.expression}`; } @@ -147,15 +147,15 @@ function formatSchedule(schedule: CronJob['schedule']): string { function formatJobSummary(job: CronJob): string { const lines = [ `ID: ${job.id}`, - `名前: ${job.name}`, - `スケジュール: ${formatSchedule(job.schedule)}`, - `プロンプト: ${job.prompt.length > 200 ? `${job.prompt.substring(0, 200)}...` : job.prompt}`, - `タイムゾーン: ${job.timezone ?? '(デフォルト)'}`, - `モデル: ${job.model ?? '(デフォルト)'}`, - `次回実行: ${job.nextRunAt ?? '(未設定)'}`, - `配信先: ${job.reply ? `${job.reply.channel}:${job.reply.channelId}` : '(home)'}`, - `自動削除: ${job.deleteAfterRun != null ? (job.deleteAfterRun ? 'はい' : 'いいえ') : '(デフォルト)'}`, - `有効: ${job.enabled ? 'はい' : 'いいえ'}`, + `Name: ${job.name}`, + `Schedule: ${formatSchedule(job.schedule)}`, + `Prompt: ${job.prompt.length > 200 ? `${job.prompt.substring(0, 200)}...` : job.prompt}`, + `Timezone: ${job.timezone ?? '(default)'}`, + `Model: ${job.model ?? '(default)'}`, + `Next run: ${job.nextRunAt ?? '(not set)'}`, + `Delivery: ${job.reply ? `${job.reply.channel}:${job.reply.channelId}` : '(home)'}`, + `Auto-delete: ${job.deleteAfterRun != null ? (job.deleteAfterRun ? 'yes' : 'no') : '(default)'}`, + `Enabled: ${job.enabled ? 'yes' : 'no'}`, ]; return lines.join('\n'); } @@ -216,7 +216,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { const summary = formatJobSummary(job); return { - content: [{ type: 'text' as const, text: `✅ ジョブを登録しました:\n\n${summary}` }], + content: [{ type: 'text' as const, text: `✅ Job registered:\n\n${summary}` }], }; } @@ -224,12 +224,12 @@ export function createCronServer(workspace: string, timezone?: string): Server { const jobs = listJobs(workspace); if (jobs.length === 0) { return { - content: [{ type: 'text' as const, text: '登録されたジョブはありません。' }], + content: [{ type: 'text' as const, text: 'No registered jobs.' }], }; } const text = jobs.map((j) => formatJobSummary(j)).join('\n\n---\n\n'); return { - content: [{ type: 'text' as const, text: `${jobs.length}件のジョブ:\n\n${text}` }], + content: [{ type: 'text' as const, text: `${jobs.length} job(s):\n\n${text}` }], }; } @@ -255,7 +255,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { ); return { - content: [{ type: 'text' as const, text: `✅ ジョブ "${id}" を削除しました。` }], + content: [{ type: 'text' as const, text: `✅ Job "${id}" removed.` }], }; } @@ -309,7 +309,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { return { content: [ - { type: 'text' as const, text: `✅ ジョブを更新しました:\n\n${formatJobSummary(updated)}` }, + { type: 'text' as const, text: `✅ Job updated:\n\n${formatJobSummary(updated)}` }, ], }; } @@ -330,7 +330,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { try { const config = loadConfig(); await fireCronJob(job, config, workspace); - return { content: [{ type: 'text' as const, text: `🚀 ジョブ "${id}" を手動実行しました。` }] }; + return { content: [{ type: 'text' as const, text: `🚀 Job "${id}" fired manually.` }] }; } catch (err) { return { content: [ @@ -352,7 +352,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { const limit = typeof params.limit === 'number' ? params.limit : 20; const entries = loadRunLog(workspace, id, limit); if (entries.length === 0) { - return { content: [{ type: 'text' as const, text: `ジョブ "${id}" の実行履歴はありません。` }] }; + return { content: [{ type: 'text' as const, text: `No run history for job "${id}".` }] }; } const lines = entries.map((e) => { const time = new Date(e.timestamp).toISOString(); @@ -361,7 +361,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { }); return { content: [ - { type: 'text' as const, text: `実行履歴 (${entries.length}件):\n\n${lines.join('\n')}` }, + { type: 'text' as const, text: `Run history (${entries.length} entries):\n\n${lines.join('\n')}` }, ], }; } diff --git a/src/skills/cli.ts b/src/skills/cli.ts index 2982145..959e9aa 100644 --- a/src/skills/cli.ts +++ b/src/skills/cli.ts @@ -1,12 +1,12 @@ /** - * cli.ts — `geminiclaw skill` サブコマンド群。 + * cli.ts — `geminiclaw skill` subcommands. * - * skill list / enable / disable / remove / scan / install / search を提供する。 - * install / search / remove は bunx skills CLI に委譲する。 + * Provides skill list / enable / disable / remove / scan / install / search. + * install / search / remove are delegated to bunx skills CLI. * - * セキュリティ: - * - install 時に staging dir でスキャン → danger はブロック、warning はユーザー確認 - * - enable/disable はリネーム方式(Gemini CLI が enabled フィールドを無視するため) + * Security: + * - On install, scan in a staging dir: danger is blocked, warning prompts user confirmation + * - enable/disable uses rename approach (Gemini CLI ignores the enabled frontmatter field) */ import { spawn } from 'node:child_process'; @@ -44,7 +44,7 @@ function printFindings(findings: SecurityFinding[]): void { } } -/** ユーザーに Y/N 確認を求める。非 TTY では安全側に倒す(reject)。 */ +/** Prompt the user for Y/N confirmation. Defaults to reject (safe side) on non-TTY. */ function askConfirmation(message: string): Promise { if (!process.stdin.isTTY) return Promise.resolve(false); return new Promise((resolve) => { @@ -203,12 +203,12 @@ export function buildSkillCommand(): Command { return; } - // safe スキル: 既に workspace に移動済み + // Safe skills: already moved to workspace for (const name of result.scanned) { process.stdout.write(` ${RISK_ICONS.safe} Installed: ${name}\n`); } - // warned スキル: findings を表示してユーザー確認 + // Warned skills: show findings and prompt user confirmation if (result.warned.length > 0 && result._stagingDir) { for (const name of result.warned) { const report = result.reports[name]; @@ -225,7 +225,7 @@ export function buildSkillCommand(): Command { } } - // blocked スキル + // Blocked skills for (const name of result.blocked) { const report = result.reports[name]; process.stderr.write(`\n${RISK_ICONS.danger} Blocked: ${name}\n`); @@ -236,7 +236,7 @@ export function buildSkillCommand(): Command { process.stdout.write('\nUse --force to install blocked skills.\n'); } - // staging クリーンアップ + // Staging cleanup if (result._stagingDir) { cleanupStaging(result._stagingDir); } diff --git a/src/skills/manager.ts b/src/skills/manager.ts index cce7ae1..acc21ed 100644 --- a/src/skills/manager.ts +++ b/src/skills/manager.ts @@ -1,14 +1,14 @@ /** - * manager.ts — スキル管理。 + * manager.ts — Skill management. * * bundled skills: {workspace}/.gemini/skills/ - * external skills: {workspace}/.agents/skills/ (bunx skills CLI 経由) + * external skills: {workspace}/.agents/skills/ (via bunx skills CLI) * - * Gemini CLI は両方のパスをネイティブに検索する。 + * Gemini CLI natively searches both paths. * - * セキュリティ: - * - install は staging dir 方式(TOCTOU 防止) - * - enable/disable はリネーム方式(Gemini CLI が enabled フィールドを無視するため) + * Security: + * - install uses staging dir approach (TOCTOU prevention) + * - enable/disable uses rename approach (Gemini CLI ignores the enabled frontmatter field) */ import { execFile } from 'node:child_process'; @@ -103,8 +103,8 @@ function externalSkillsDir(workspaceDir: string): string { } /** - * ディレクトリ内のスキルを読み取る共通ヘルパー。 - * disabled 状態(SKILL.md.disabled)のスキルも含める。 + * Common helper to read skills from a directory. + * Includes disabled skills (SKILL.md.disabled). */ function readSkillsFromDir(dir: string, source: 'bundled' | 'external'): SkillFrontmatter[] { if (!existsSync(dir)) return []; @@ -116,7 +116,7 @@ function readSkillsFromDir(dir: string, source: 'bundled' | 'external'): SkillFr if (!entry.isDirectory()) continue; const dirPath = join(dir, entry.name); - // disabled 状態のスキル + // Disabled skills const disabledPath = join(dirPath, SKILL_FILENAME + SKILL_DISABLED_SUFFIX); if (existsSync(disabledPath)) { const content = readFileSync(disabledPath, 'utf-8'); @@ -140,13 +140,13 @@ function readSkillsFromDir(dir: string, source: 'bundled' | 'external'): SkillFr } /** - * bundled + external スキルの一覧を返す。 + * Return a list of all bundled + external skills. */ export async function listSkills(workspaceDir: string): Promise { const bundled = readSkillsFromDir(bundledSkillsDir(workspaceDir), 'bundled'); const external = readSkillsFromDir(externalSkillsDir(workspaceDir), 'external'); - // 同名スキルは bundled が優先 + // Bundled skills take precedence for same-name conflicts const bundledNames = new Set(bundled.map((s) => s.name)); const deduped = external.filter((s) => !bundledNames.has(s.name)); @@ -154,8 +154,8 @@ export async function listSkills(workspaceDir: string): Promise { validateSkillName(name); @@ -166,7 +166,7 @@ export async function enableSkill(name: string, workspaceDir: string): Promise { validateSkillName(name); @@ -186,7 +186,7 @@ export async function disableSkill(name: string, workspaceDir: string): Promise< renameSync(skillMdPath, join(dir, name, SKILL_FILENAME + SKILL_DISABLED_SUFFIX)); return; } - // 既に無効な場合は何もしない + // Already disabled — no-op if (existsSync(join(dir, name, SKILL_FILENAME + SKILL_DISABLED_SUFFIX))) return; } @@ -194,7 +194,7 @@ export async function disableSkill(name: string, workspaceDir: string): Promise< } /** - * スキルを削除する。external スキルは bunx skills remove に委譲。 + * Remove a skill. External skills are delegated to bunx skills remove. */ export async function removeSkill(name: string, workspaceDir: string): Promise { validateSkillName(name); @@ -216,14 +216,14 @@ export async function removeSkill(name: string, workspaceDir: string): Promise 0) { moveSkillsToWorkspace(stagingSkillsDir, toMove, workspaceDir); @@ -299,14 +299,14 @@ export async function installSkill( return { installed: newSkills, scanned, warned, blocked, reports, _stagingDir: stagingDir }; } catch (err) { - // エラー時は staging を確実にクリーンアップ + // Ensure staging is cleaned up on error rmSync(stagingDir, { recursive: true, force: true }); throw err; } } /** - * warning スキルのユーザー確認後に呼び出し、staging → workspace に移動する。 + * Called after user confirmation for warned skills, moves from staging to workspace. */ export function confirmInstall(stagingDir: string, skillNames: string[], workspaceDir: string): void { const stagingSkillsDir = join(stagingDir, '.agents', 'skills'); @@ -314,7 +314,7 @@ export function confirmInstall(stagingDir: string, skillNames: string[], workspa } /** - * staging のクリーンアップ。install 完了後に呼び出す。 + * Clean up the staging directory. Called after install completes. */ export function cleanupStaging(stagingDir: string): void { if (existsSync(stagingDir)) { @@ -328,7 +328,7 @@ export interface InstallResult { warned: string[]; blocked: string[]; reports: Record; - /** staging dir パス。warned スキルの確認後に confirmInstall() で使用。 */ + /** Staging dir path. Used by confirmInstall() after warned skill confirmation. */ _stagingDir?: string; } @@ -361,7 +361,7 @@ function findSkillMdPath(name: string, workspaceDir: string): string | null { return null; } -/** staging → workspace の .agents/skills/ にスキルディレクトリを移動する。 */ +/** Move skill directories from staging to workspace .agents/skills/. */ function moveSkillsToWorkspace(stagingSkillsDir: string, names: string[], workspaceDir: string): void { const targetDir = externalSkillsDir(workspaceDir); mkdirSync(targetDir, { recursive: true }); @@ -376,7 +376,7 @@ function moveSkillsToWorkspace(stagingSkillsDir: string, names: string[], worksp } } -/** staging の skills-lock.json を workspace にコピー(マージ)。 */ +/** Copy (merge) skills-lock.json from staging to workspace. */ function copyLockFile(stagingDir: string, workspaceDir: string): void { const srcLock = join(stagingDir, 'skills-lock.json'); if (!existsSync(srcLock)) return; @@ -400,7 +400,7 @@ function copyLockFile(stagingDir: string, workspaceDir: string): void { writeFileSync(destLock, `${JSON.stringify(merged, null, 2)}\n`, 'utf-8'); } catch { - // ロックファイルのマージ失敗は致命的ではない + // Lock file merge failure is non-fatal } } diff --git a/src/skills/scanner.ts b/src/skills/scanner.ts index e6cc67d..a43d4e8 100644 --- a/src/skills/scanner.ts +++ b/src/skills/scanner.ts @@ -1,17 +1,17 @@ /** - * scanner.ts — スキルのセキュリティスキャナー。 + * scanner.ts — Security scanner for skills. * - * 3層防御アーキテクチャ: - * 層1: 決定論的静的パターンスキャン(riskLevel を決定する唯一の層) - * 層2: LLM による補助分析(advisory のみ、riskLevel を変更しない) - * 層3: ランタイムサンドボックス(Seatbelt — scanner 外で実施) + * Three-layer defense architecture: + * Layer 1: Deterministic static pattern scan (the only layer that determines riskLevel) + * Layer 2: LLM-assisted analysis (advisory only, does not change riskLevel) + * Layer 3: Runtime sandbox (Seatbelt — handled outside the scanner) * - * 静的パターンは ClawHavoc (CVE-2026-25253), Skill-Inject (arXiv:2602.20156), - * Cursor Unicode 攻撃, Claude Code 設定インジェクション (CVE-2025-59536) の - * 実世界攻撃ベクターに基づく。 + * Static patterns are based on real-world attack vectors from ClawHavoc (CVE-2026-25253), + * Skill-Inject (arXiv:2602.20156), Cursor Unicode attacks, and Claude Code config + * injection (CVE-2025-59536). * - * LLM-as-judge は adversarial スキルに対して脆弱であることが学術的に実証されている - * (Lakera, arXiv:2505.13348) ため、riskLevel の判定には使用しない。 + * LLM-as-judge has been academically proven vulnerable to adversarial skills + * (Lakera, arXiv:2505.13348), so it is not used for riskLevel determination. */ import { readdirSync, readFileSync, statSync } from 'node:fs'; @@ -26,7 +26,7 @@ interface PatternRule { severity: RiskLevel; } -// ── DANGER: インストールをブロックするパターン ──────────────────── +// ── DANGER: Patterns that block installation ───────────────────── const DANGER_PATTERNS: PatternRule[] = [ // Remote code execution chains @@ -81,7 +81,7 @@ const DANGER_PATTERNS: PatternRule[] = [ }, ]; -// ── WARNING: ユーザー確認を促すパターン ────────────────────────── +// ── WARNING: Patterns that prompt user confirmation ────────────── const WARNING_PATTERNS: PatternRule[] = [ // External HTTP requests @@ -143,8 +143,8 @@ const WARNING_PATTERNS: PatternRule[] = [ }, ]; -// ── SKILL.md プロンプトインジェクション検査パターン ─────────────── -// Skill-Inject (arXiv:2602.20156) + ClawHavoc + Cursor Unicode 攻撃に基づく +// ── SKILL.md prompt injection detection patterns ───────────────── +// Based on Skill-Inject (arXiv:2602.20156) + ClawHavoc + Cursor Unicode attacks const PROMPT_INJECTION_PATTERNS: PatternRule[] = [ // Instruction override @@ -222,7 +222,7 @@ const PROMPT_INJECTION_PATTERNS: PatternRule[] = [ }, ]; -// ── Unicode 難読化検出(Cursor ルールファイル攻撃に基づく) ──────── +// ── Unicode obfuscation detection (based on Cursor rule file attacks) ── const UNICODE_OBFUSCATION_PATTERNS: PatternRule[] = [ { @@ -240,7 +240,7 @@ const UNICODE_OBFUSCATION_PATTERNS: PatternRule[] = [ ]; /** - * ディレクトリ内のテキストファイルを再帰的に収集する。 + * Recursively collect text files within a directory. */ function collectFiles(dir: string): string[] { const results: string[] = []; @@ -257,13 +257,13 @@ function collectFiles(dir: string): string[] { } } } catch { - // ディレクトリ読み取り失敗は無視 + // Ignore directory read failures } return results; } /** - * 単一ファイルを静的パターンでスキャンする。 + * Scan a single file against static patterns. */ function scanFile(filePath: string, patterns: PatternRule[]): SecurityFinding[] { let content: string; @@ -294,10 +294,10 @@ function scanFile(filePath: string, patterns: PatternRule[]): SecurityFinding[] } /** - * LLM による補助的セキュリティ分析。 + * LLM-assisted supplementary security analysis. * - * advisory のみを返す。riskLevel の判定には使用しない。 - * コンテンツは base64 エンコードして LLM に渡し、adversarial injection 耐性を向上させる。 + * Returns advisory only. Not used for riskLevel determination. + * Content is base64-encoded before passing to the LLM to improve adversarial injection resistance. */ async function scanWithLLM(files: string[], model: string, workspacePath: string): Promise { const { spawnGeminiAcp } = await import('../agent/acp/runner.js'); @@ -311,11 +311,11 @@ async function scanWithLLM(files: string[], model: string, workspacePath: string if (combined.length + chunk.length > MAX_CONTENT_CHARS) break; combined += chunk; } catch { - // 読み取り失敗ファイルはスキップ + // Skip files that fail to read } } - // base64 エンコードで content を命令として解釈されにくくする + // Base64-encode content to prevent interpretation as instructions const encoded = Buffer.from(combined).toString('base64'); const prompt = `You are a security auditor reviewing skill files for an AI agent system. @@ -352,10 +352,10 @@ If no concerns, respond: {"concerns": [], "safe": true}`; } /** - * スキルディレクトリをスキャンしてセキュリティレポートを返す。 + * Scan a skill directory and return a security report. * - * riskLevel は静的パターンスキャンのみで決定論的に決まる。 - * LLM レビューは advisory としてのみ提供され、riskLevel を変更しない。 + * riskLevel is determined deterministically by static pattern scanning only. + * LLM review is provided as advisory only and does not change riskLevel. */ export async function scanSkill( skillDir: string, @@ -368,7 +368,7 @@ export async function scanSkill( for (const filePath of files) { const isSkillMd = filePath.endsWith('SKILL.md') || filePath.endsWith('SKILL.md.pending'); - // SKILL.md はプロンプトインジェクション + Unicode 難読化検査も行う + // SKILL.md also undergoes prompt injection + Unicode obfuscation checks const patterns = isSkillMd ? [...DANGER_PATTERNS, ...WARNING_PATTERNS, ...PROMPT_INJECTION_PATTERNS, ...UNICODE_OBFUSCATION_PATTERNS] : [...DANGER_PATTERNS, ...WARNING_PATTERNS, ...UNICODE_OBFUSCATION_PATTERNS]; @@ -376,7 +376,7 @@ export async function scanSkill( findings.push(...scanFile(filePath, patterns)); } - // 静的スキャンのみでリスクレベルを決定(決定論的) + // Determine risk level from static scan only (deterministic) let riskLevel: RiskLevel = 'safe'; if (findings.some((f) => f.severity === 'danger')) { riskLevel = 'danger'; @@ -384,7 +384,7 @@ export async function scanSkill( riskLevel = 'warning'; } - // LLM レビュー(advisory のみ — riskLevel を変更しない) + // LLM review (advisory only — does not change riskLevel) let llmAdvisory: string | undefined; if (!options?.skipLlm && options?.workspacePath) { llmAdvisory = await scanWithLLM(files, model, options.workspacePath); diff --git a/src/skills/types.ts b/src/skills/types.ts index 421a232..43b4184 100644 --- a/src/skills/types.ts +++ b/src/skills/types.ts @@ -1,7 +1,7 @@ /** - * types.ts — スキルシステムのコア型定義。 + * types.ts — Core type definitions for the skill system. * - * スキルのフロントマター・セキュリティスキャン結果を表す型を定義する。 + * Defines types for skill frontmatter and security scan results. */ export interface SkillFrontmatter { @@ -9,9 +9,9 @@ export interface SkillFrontmatter { description: string; enabled: boolean; source?: 'bundled' | 'installed' | 'external'; - /** インストール日時 (ISO 8601) */ + /** Installation date (ISO 8601) */ installedAt?: string; - /** 'openclaw/jdrhyne/todo-tracker@1.0.0' 形式の参照 */ + /** Reference in 'openclaw/jdrhyne/todo-tracker@1.0.0' format */ sourceRef?: string; } @@ -26,10 +26,10 @@ export interface SecurityFinding { } export interface SecurityReport { - /** 決定論的静的パターンのみで決定されるリスクレベル */ + /** Risk level determined solely by deterministic static patterns */ riskLevel: RiskLevel; findings: SecurityFinding[]; - /** LLM による補助的セキュリティ分析(advisory のみ、riskLevel を変更しない) */ + /** LLM-assisted supplementary security analysis (advisory only, does not change riskLevel) */ llmAdvisory?: string; scannedAt: string; } diff --git a/src/tui/pi/chat-log.ts b/src/tui/pi/chat-log.ts index b51449c..9db9d79 100644 --- a/src/tui/pi/chat-log.ts +++ b/src/tui/pi/chat-log.ts @@ -166,12 +166,12 @@ export class ChatLogComponent implements Component { const hintParts: string[] = []; const hasThink = this._chunks.some((c) => c.kind === 'think'); if (hasThink) { - hintParts.push(mutedText(`[Ctrl+O] 思考${this.showThinking ? 'ON' : 'OFF'}`)); + hintParts.push(mutedText(`[Ctrl+O] Thinking ${this.showThinking ? 'ON' : 'OFF'}`)); } if (!isAtBottom) { - hintParts.push(chalk.yellow.dim(`↑ [${startIdx + 1}–${endIdx}/${total}] PgDn で最新へ`)); + hintParts.push(chalk.yellow.dim(`↑ [${startIdx + 1}–${endIdx}/${total}] PgDn to latest`)); } else if (total > displayHeight) { - hintParts.push(mutedText('PgUp / Opt+↑ でスクロール')); + hintParts.push(mutedText('PgUp / Opt+↑ to scroll')); } const hint = padToWidth(` ${hintParts.join(' ')}`, width); diff --git a/src/tui/pi/debug-overlay.ts b/src/tui/pi/debug-overlay.ts index 8c64ba9..15abc9b 100644 --- a/src/tui/pi/debug-overlay.ts +++ b/src/tui/pi/debug-overlay.ts @@ -133,12 +133,12 @@ export class DebugOverlayComponent implements Component { const rows: string[] = []; // Title bar - const title = ` ${toolTitle.bold('Gemini CLI Live')} ${mutedText('[Ctrl+D] 閉じる [↑/↓] スクロール')}`; + const title = ` ${toolTitle.bold('Gemini CLI Live')} ${mutedText('[Ctrl+D] Close [↑/↓] Scroll')}`; rows.push(padToWidth(title, w)); rows.push(padToWidth(borderDim('─'.repeat(w)), w)); if (this.entries.length === 0) { - rows.push(padToWidth(mutedText(' Gemini CLIイベント待機中…'), w)); + rows.push(padToWidth(mutedText(' Waiting for Gemini CLI events…'), w)); rows.push(padToWidth(borderDim('─'.repeat(w)), w)); return rows; } @@ -165,8 +165,8 @@ export class DebugOverlayComponent implements Component { // Scroll indicator const atBottom = offset === 0; const scrollHint = atBottom - ? mutedText(` 最新 ${total} events`) - : chalk.yellow.dim(` [${startIdx + 1}–${endIdx}/${total}] ↓ 最新へ`); + ? mutedText(` Latest ${total} events`) + : chalk.yellow.dim(` [${startIdx + 1}–${endIdx}/${total}] ↓ To latest`); rows.push(padToWidth(borderDim('─'.repeat(w)), w)); rows.push(padToWidth(scrollHint, w)); diff --git a/src/tui/pi/interactive-app.ts b/src/tui/pi/interactive-app.ts index 24c44ae..2936c82 100644 --- a/src/tui/pi/interactive-app.ts +++ b/src/tui/pi/interactive-app.ts @@ -44,16 +44,16 @@ class HintLine implements Component { let text: string; if (this.confirmClear) { text = - chalk.yellow(' チャット履歴を削除します。') + + chalk.yellow(' Clear chat history.') + chalk.white.bold(' [Enter]') + - chalk.yellow(' 確認 ') + + chalk.yellow(' Confirm ') + chalk.white.bold('[Esc]') + - chalk.yellow(' キャンセル'); + chalk.yellow(' Cancel'); } else if (this.disabled) { - text = mutedText(' エージェント実行中…'); + text = mutedText(' Agent running…'); } else { text = mutedText( - ' [Shift+Enter] 改行 [Ctrl+L] クリア [Ctrl+G] デバッグ [Ctrl+M] MCP [Ctrl+W] ファイル [Ctrl+C] 終了', + ' [Shift+Enter] Newline [Ctrl+L] Clear [Ctrl+G] Debug [Ctrl+M] MCP [Ctrl+W] Files [Ctrl+C] Quit', ); } return [padToWidth(text, width)]; diff --git a/src/tui/pi/workspace-viewer.ts b/src/tui/pi/workspace-viewer.ts index 71aad06..eb59c12 100644 --- a/src/tui/pi/workspace-viewer.ts +++ b/src/tui/pi/workspace-viewer.ts @@ -115,12 +115,12 @@ export class WorkspaceViewerComponent implements Component { private _renderList(w: number): string[] { const rows: string[] = []; - const hint = mutedText('[↑↓] 選択 [Enter] 開く [Esc] 閉じる'); + const hint = mutedText('[↑↓] Select [Enter] Open [Esc] Close'); rows.push(padToWidth(` ${toolTitle.bold('Workspace Files')} ${hint}`, w)); rows.push(padToWidth(borderDim('─'.repeat(w)), w)); if (!this._loaded || this.files.length === 0) { - rows.push(padToWidth(mutedText(' ファイルなし'), w)); + rows.push(padToWidth(mutedText(' No files'), w)); } else { for (let i = 0; i < this.files.length; i++) { const selected = i === this.selectedIdx; @@ -151,7 +151,7 @@ export class WorkspaceViewerComponent implements Component { const visible = reflowed.slice(scroll, scroll + MAX_CONTENT); while (visible.length < MAX_CONTENT) visible.push(''); - const hint = mutedText('[↑↓/PgUp/PgDn] スクロール [Esc] 一覧へ'); + const hint = mutedText('[↑↓/PgUp/PgDn] Scroll [Esc] Back to list'); const rows: string[] = []; rows.push(padToWidth(` ${accent.bold(this.detailFile)} ${hint}`, w)); rows.push(padToWidth(borderDim('─'.repeat(w)), w)); @@ -162,8 +162,8 @@ export class WorkspaceViewerComponent implements Component { const scrollHint = total > MAX_CONTENT - ? mutedText(` [${scroll + 1}–${Math.min(scroll + MAX_CONTENT, total)}/${total}行]`) - : mutedText(` ${total}行`); + ? mutedText(` [${scroll + 1}–${Math.min(scroll + MAX_CONTENT, total)}/${total} lines]`) + : mutedText(` ${total} lines`); rows.push(padToWidth(borderDim('─'.repeat(w)), w)); rows.push(padToWidth(scrollHint, w)); return rows; @@ -174,7 +174,7 @@ export class WorkspaceViewerComponent implements Component { const raw = await readFile(join(this.workspacePath, filename), 'utf-8'); this.detailLines = raw.split('\n'); } catch { - this.detailLines = ['(読み込みエラー)']; + this.detailLines = ['(Failed to read file)']; } this.detailFile = filename; this.detailScroll = 0; diff --git a/templates/.gemini/skills/agent-browser/references/site-patterns.md b/templates/.gemini/skills/agent-browser/references/site-patterns.md index 4599b48..f7664e3 100644 --- a/templates/.gemini/skills/agent-browser/references/site-patterns.md +++ b/templates/.gemini/skills/agent-browser/references/site-patterns.md @@ -6,7 +6,7 @@ Patterns for sites with known characteristics. **This file is auto-updated** by - [Amazon](#amazon-amazoncojp--amazoncoma) - [General Patterns by Site Type](#general-patterns-by-site-type) -> 5〜6サイトを超えたら site-patterns/ ディレクトリに分割を検討する。 +> Consider splitting into a site-patterns/ directory once this exceeds 5-6 sites. --- @@ -60,22 +60,22 @@ Prefer clicking **sidebar filters** over constructing URLs manually: agent-browser open "https://www.amazon.co.jp/s?k=&i=" agent-browser wait 2000 # Find and click filters from the snapshot instead of URL hacking -agent-browser snapshot -i | grep -E "過去7日|過去30日|カテゴリー|絞り込み" +agent-browser snapshot -i | grep -E "Past 7 days|Past 30 days|Category|Filter" agent-browser click @eN # click the filter ``` Useful URL parameters when building search URLs: -- `i=digital-text` — Kindleストア -- `i=stripbooks` — 本・紙書籍 -- `s=date-desc-rank` — 発売日の新しい順 -- `s=review-rank` — レビュー評価順 -- `page=N` — ページ番号 +- `i=digital-text` — Kindle Store +- `i=stripbooks` — Books (print) +- `s=date-desc-rank` — Newest release date first +- `s=review-rank` — Review rating order +- `page=N` — Page number ### Extracting Search Results ```bash # Efficient: grep snapshot for titles and status -agent-browser snapshot -i | grep -E "link.*コミック|link.*文庫|heading.*件" +agent-browser snapshot -i | grep -E "link.*Comic|link.*Paperback|heading.*results" # For structured data (title + price + status across many cards): agent-browser eval --stdin <<'EOF' @@ -85,10 +85,10 @@ document.querySelectorAll('[data-component-type="s-search-result"]').forEach(fun var price = el.querySelector('.a-price .a-offscreen') ? el.querySelector('.a-price .a-offscreen').textContent.trim() : ''; var asin = el.getAttribute('data-asin') || ''; var text = el.innerText; - var dateMatch = text.match(/202\d年\d+月\d+日/); - var available = text.indexOf('今すぐ買う') !== -1 || text.indexOf('すぐに購読可能') !== -1; - var preorder = text.indexOf('発売予定日') !== -1; - items.push(title.substring(0,60) + '|||' + price + '|||' + (dateMatch ? dateMatch[0] : '既発売') + '|||' + (preorder ? '予約' : available ? '購入可' : '?') + '|||' + asin); + var dateMatch = text.match(/\w+ \d+, 202\d/) || text.match(/202\d年\d+月\d+日/); + var available = text.indexOf('Buy now') !== -1 || text.indexOf('Available instantly') !== -1; + var preorder = text.indexOf('Pre-order') !== -1; + items.push(title.substring(0,60) + '|||' + price + '|||' + (dateMatch ? dateMatch[0] : 'Released') + '|||' + (preorder ? 'Pre-order' : available ? 'Available' : '?') + '|||' + asin); }); items.join('\n'); EOF @@ -111,7 +111,7 @@ agent-browser eval 'document.querySelector(".a-price .a-offscreen")?.textContent ```bash # Find and click "Next page" from snapshot -agent-browser snapshot -i | grep -E "次のページ|次へ|button.*ページ" +agent-browser snapshot -i | grep -E "Next page|Next|button.*page" agent-browser click @eN agent-browser wait 2000 ``` @@ -125,7 +125,7 @@ agent-browser wait 2000 - Avoid `networkidle` — they load ads/tracking continuously - Look for sidebar date/category filters before constructing filter URLs - Product cards usually share a common CSS class — use `eval` to batch-extract -- Pagination: look for `次へ` / `next` button in snapshot +- Pagination: look for `Next` / `next` button in snapshot ### News / Blog Sites diff --git a/templates/.gemini/skills/translate-preview/SKILL.md b/templates/.gemini/skills/translate-preview/SKILL.md index c5cfee2..1ed0a36 100644 --- a/templates/.gemini/skills/translate-preview/SKILL.md +++ b/templates/.gemini/skills/translate-preview/SKILL.md @@ -1,12 +1,12 @@ --- name: translate-preview -description: Translate a web page while preserving its original DOM structure, CSS, and images. Generates a self-contained bilingual preview HTML with toggle controls. Use this skill whenever the user shares a URL and asks for translation, translated preview, bilingual view, or when the channel topic instructs to use translate-preview. Also triggers on Japanese phrases like "翻訳プレビュー", "翻訳して", "日本語で読みたい". Do NOT write your own translation scripts — always use this skill's reference files. +description: Translate a web page while preserving its original DOM structure, CSS, and images. Generates a self-contained bilingual preview HTML with toggle controls. Use this skill whenever the user shares a URL and asks for translation, translated preview, bilingual view, or when the channel topic instructs to use translate-preview. Do NOT write your own translation scripts — always use this skill's reference files. enabled: true --- # Translate Preview -Translate a web page into a bilingual preview HTML with toggle controls (訳文/対訳/原文). +Translate a web page into a bilingual preview HTML with toggle controls (Translated/Both/Original). ## Flow @@ -27,8 +27,8 @@ Translate all blocks at once to preserve cross-paragraph context. Save to `runs/ **Output format**: Array of objects matching blocks.json structure with `translatedText` added: ```json [ - {"id": 0, "type": "paragraph", "translatedText": "翻訳されたテキスト"}, - {"id": 1, "type": "heading", "translatedText": "見出しの翻訳"} + {"id": 0, "type": "paragraph", "translatedText": "Translated text"}, + {"id": 1, "type": "heading", "translatedText": "Translated heading"} ] ``` diff --git a/templates/.gemini/skills/translate-preview/references/build-injection.js b/templates/.gemini/skills/translate-preview/references/build-injection.js index 70beb37..3626b7c 100644 --- a/templates/.gemini/skills/translate-preview/references/build-injection.js +++ b/templates/.gemini/skills/translate-preview/references/build-injection.js @@ -38,7 +38,7 @@ function buildInjectionPayload(blocksData, translatedData) { blocks: mergedBlocks, sourceUrl: blocksData.url || '', title: blocksData.title || '', - targetLang: translatedData.targetLang || 'ja', + targetLang: translatedData.targetLang || '', }; // Include extract-blocks.js so extractBlocks() is available for DOM re-marking diff --git a/templates/.gemini/skills/translate-preview/references/inject-translations.js b/templates/.gemini/skills/translate-preview/references/inject-translations.js index 20979bb..8e23f0a 100644 --- a/templates/.gemini/skills/translate-preview/references/inject-translations.js +++ b/templates/.gemini/skills/translate-preview/references/inject-translations.js @@ -80,9 +80,9 @@ function _tp_injectUIOverlay(doc, data) { (data.sourceUrl ? '' + _tp_escapeHtml(data.sourceUrl) + '' : '') + '' + '
' + - '' + - '' + - '' + + '' + + '' + + '' + '
' + ''; diff --git a/templates/.gemini/skills/translate-preview/references/render.js b/templates/.gemini/skills/translate-preview/references/render.js index 4b9c342..a885dea 100644 --- a/templates/.gemini/skills/translate-preview/references/render.js +++ b/templates/.gemini/skills/translate-preview/references/render.js @@ -40,7 +40,7 @@ if (blocksData.domain === 'x.com') { { sourceUrl: blocksData.url || '', title: blocksData.title || '', - targetLang: translatedData.targetLang || 'ja', + targetLang: translatedData.targetLang || '', } ); process.stdout.write(html); diff --git a/templates/.gemini/skills/translate-preview/references/twitter-render.js b/templates/.gemini/skills/translate-preview/references/twitter-render.js index 59b4a93..59e4538 100644 --- a/templates/.gemini/skills/translate-preview/references/twitter-render.js +++ b/templates/.gemini/skills/translate-preview/references/twitter-render.js @@ -9,7 +9,7 @@ // // Produces the same bilingual UI as inject-translations.js: // - tp-block wrappers with tp-original / tp-translated -// - Fixed header with 訳文/対訳/原文 toggle buttons +// - Fixed header with Translated/Both/Original toggle buttons // - Dark mode support // - Long-press and right-click individual block toggle @@ -66,7 +66,7 @@ function renderTwitterHtml(blocks, translatedBlocks, options) { var opts = options || {}; var sourceUrl = opts.sourceUrl || ''; var title = opts.title || ''; - var targetLang = opts.targetLang || 'ja'; + var targetLang = opts.targetLang || ''; // Build translation lookup: id → translated text // Accept three formats: @@ -123,9 +123,9 @@ function _tr_getHeaderHtml(sourceUrl) { urlHtml + '' + '
' + - '' + - '' + - '' + + '' + + '' + + '' + '
' + '' + ''; @@ -263,7 +263,7 @@ if (typeof process !== 'undefined' && process.argv && process.argv[1] && { sourceUrl: blocksData.url || '', title: blocksData.title || '', - targetLang: translatedData.targetLang || 'ja', + targetLang: translatedData.targetLang || '', } ); From 3c46d1a78b6b49c3baa0f06122db7dd1fc26f42e Mon Sep 17 00:00:00 2001 From: e-mon Date: Fri, 13 Mar 2026 17:10:39 +0900 Subject: [PATCH 4/4] style: fix biome formatting after i18n changes Co-Authored-By: Claude Opus 4.6 --- src/cli/commands/browser.ts | 4 +--- src/mcp/cron-server.ts | 9 +++++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/browser.ts b/src/cli/commands/browser.ts index e9578fe..e8c605b 100644 --- a/src/cli/commands/browser.ts +++ b/src/cli/commands/browser.ts @@ -35,9 +35,7 @@ function abRun(args: string[]): boolean { if (result.error) { const err = result.error as NodeJS.ErrnoException; if (err.code === 'ENOENT') { - process.stderr.write( - 'agent-browser not found. Install it with: bun i -g agent-browser\n', - ); + process.stderr.write('agent-browser not found. Install it with: bun i -g agent-browser\n'); } else { process.stderr.write(`agent-browser error: ${err.message}\n`); } diff --git a/src/mcp/cron-server.ts b/src/mcp/cron-server.ts index 4862a61..7b2f81b 100644 --- a/src/mcp/cron-server.ts +++ b/src/mcp/cron-server.ts @@ -308,9 +308,7 @@ export function createCronServer(workspace: string, timezone?: string): Server { } return { - content: [ - { type: 'text' as const, text: `✅ Job updated:\n\n${formatJobSummary(updated)}` }, - ], + content: [{ type: 'text' as const, text: `✅ Job updated:\n\n${formatJobSummary(updated)}` }], }; } @@ -361,7 +359,10 @@ export function createCronServer(workspace: string, timezone?: string): Server { }); return { content: [ - { type: 'text' as const, text: `Run history (${entries.length} entries):\n\n${lines.join('\n')}` }, + { + type: 'text' as const, + text: `Run history (${entries.length} entries):\n\n${lines.join('\n')}`, + }, ], }; }