From e26cccce31ea37cb59be0adc6c6a40437dae588d Mon Sep 17 00:00:00 2001 From: azu Date: Wed, 14 Jan 2026 21:18:21 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20Claude=20Code=E9=80=A3=E6=90=BA?= =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit URLを入力するとClaude Code CLIを使ってJSer.info風の説明文を自動生成する機能を追加。 - ClaudeCodeButton: URL横のボタンでClaude Code実行 - ClaudeCodePreview: 生成結果のプレビュー表示 - Cmd+Shift+Jで結果をEditorに挿入 - MCP設定によるjser-infoツールの利用 Co-Authored-By: Claude Opus 4.5 --- css/index.css | 172 +++++++++++++++++++++ src/browser/Action/ServiceAction.js | 134 ++++++++++++++++ src/browser/Action/ServiceActionConst.js | 8 +- src/browser/App.js | 35 ++++- src/browser/Store/ServiceStore.js | 79 +++++++++- src/browser/component/ClaudeCodeButton.js | 109 +++++++++++++ src/browser/component/ClaudeCodePreview.js | 42 +++++ src/browser/component/Editor.js | 11 +- src/browser/service-instance.js | 14 ++ 9 files changed, 597 insertions(+), 7 deletions(-) create mode 100644 src/browser/component/ClaudeCodeButton.js create mode 100644 src/browser/component/ClaudeCodePreview.js diff --git a/css/index.css b/css/index.css index f54b07c..14514da 100644 --- a/css/index.css +++ b/css/index.css @@ -165,3 +165,175 @@ text-shadow: -1px 1px #417cb8; border: none; } + +/* URL入力とClaude Codeボタンの行 */ +.URLInputRow { + display: flex; + align-items: center; + gap: 8px; + margin: 0.1em 0; +} + +.URLInputRow .URLInput { + flex: 1; + margin: 0; +} + +/* Claude Codeボタン */ +.ClaudeCodeButton { + padding: 6px 8px; + font-size: 12px; + font-weight: bold; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #f5f5f5; + color: #666; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; + min-width: 36px; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + gap: 2px; +} + +.ClaudeCodeButton svg { + vertical-align: middle; +} + +.ClaudeCodeButton:hover:not(:disabled) { + background-color: #e0e0e0; + border-color: #999; +} + +.ClaudeCodeButton:disabled { + cursor: not-allowed; + opacity: 0.7; +} + +.ClaudeCodeButton--loading { + background-color: #fff3cd; + border-color: #ffc107; + color: #856404; +} + +.ClaudeCodeButton--complete { + background-color: #d4edda; + border-color: #28a745; + color: #155724; +} + +.ClaudeCodeButton--complete:hover:not(:disabled) { + background-color: #c3e6cb; + border-color: #1e7e34; +} + +.ClaudeCodeButton--error { + background-color: #f8d7da; + border-color: #dc3545; + color: #721c24; +} + +.ClaudeCodeButton-loading { + display: flex; + align-items: center; + gap: 4px; +} + +.ClaudeCodeButton-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #856404; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Claude Code Preview */ +.ClaudeCodePreview { + margin: 0.5em 0; + padding: 8px 12px; + border-radius: 4px; + font-size: 13px; +} + +.ClaudeCodePreview-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; + font-weight: bold; + font-size: 12px; +} + +.ClaudeCodePreview-hint { + font-weight: normal; + color: #666; + font-size: 11px; +} + +.ClaudeCodePreview-close { + margin-left: auto; + background: none; + border: none; + font-size: 16px; + cursor: pointer; + color: #999; + padding: 0 4px; +} + +.ClaudeCodePreview-close:hover { + color: #333; +} + +.ClaudeCodePreview-content { + white-space: pre-wrap; + line-height: 1.5; + font-family: Monaco, "Andale Mono", monospace; +} + +.ClaudeCodePreview--loading { + background-color: #fff8e1; + border: 1px solid #ffcc02; +} + +.ClaudeCodePreview--loading .ClaudeCodePreview-spinner { + display: inline-block; + width: 12px; + height: 12px; + border: 2px solid #f0a000; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.ClaudeCodePreview--complete { + background-color: #e8f5e9; + border: 1px solid #4caf50; +} + +.ClaudeCodePreview--complete .ClaudeCodePreview-content { + cursor: pointer; +} + +.ClaudeCodePreview--complete .ClaudeCodePreview-content:hover { + background-color: rgba(76, 175, 80, 0.1); +} + +.ClaudeCodePreview--error { + background-color: #ffebee; + border: 1px solid #f44336; +} + +.ClaudeCodePreview--error .ClaudeCodePreview-content { + color: #c62828; +} diff --git a/src/browser/Action/ServiceAction.js b/src/browser/Action/ServiceAction.js index 9ec2328..be53bc2 100644 --- a/src/browser/Action/ServiceAction.js +++ b/src/browser/Action/ServiceAction.js @@ -9,6 +9,10 @@ import { show as LoadingShow, dismiss as LoadingDismiss } from "../view-util/Loa import RelatedItemModel from "../models/RelatedItemModel"; import serviceInstance from "../service-instance"; +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); + export default class ServiceAction extends Action { fetchTags(service) { const client = serviceInstance.getClient(service); @@ -191,4 +195,134 @@ export default class ServiceAction extends Action { resetField() { this.dispatch(keys.resetField); } + + // Claude Code関連アクション + runClaudeCode(url, config) { + if (!config || !config.enabled) { + console.log("[ClaudeCode] Disabled or no config"); + return; + } + + // CLIが存在するかチェック + const cliPath = config.cliPath; + if (!fs.existsSync(cliPath)) { + console.warn(`[ClaudeCode] CLI not found at ${cliPath}`); + return; + } + + console.log(`[ClaudeCode] Starting for URL: ${url}`); + console.log(`[ClaudeCode] CLI: ${cliPath}`); + console.log(`[ClaudeCode] WorkDir: ${config.workDir}`); + + const workDir = config.workDir; + + // 作業ディレクトリの存在確認 + if (!fs.existsSync(workDir)) { + console.error(`[ClaudeCode] WorkDir does not exist: ${workDir}`); + this.dispatch(keys.claudeCodeError, { url, error: `WorkDir does not exist: ${workDir}` }); + return; + } + console.log(`[ClaudeCode] WorkDir exists: OK`); + + this.dispatch(keys.claudeCodeStart, { url }); + + // 設定からプロンプトを使用 + const prompt = `${config.prompt}\n\nURL: ${url}`; + + // Claude Code CLIを--print付きで実行(ワンショットモード) + // ツール許可オプションを追加 + const args = []; + + // MCP設定がある場合は追加 + if (config.mcpConfig) { + const mcpJson = JSON.stringify(config.mcpConfig); + args.push("--mcp-config", mcpJson); + console.log(`[ClaudeCode] MCP config: ${mcpJson.slice(0, 100)}...`); + } + + args.push("--print", "--dangerously-skip-permissions", prompt); + console.log( + `[ClaudeCode] Spawning: ${cliPath} ${ + config.mcpConfig ? "--mcp-config " : "" + }--print --dangerously-skip-permissions ` + ); + console.log(`[ClaudeCode] Prompt length: ${prompt.length} chars`); + console.log(`[ClaudeCode] Prompt (last 200 chars): ...${prompt.slice(-200)}`); + + const spawnEnv = { + ...process.env, + HOME: process.env.HOME, + PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + process.env.PATH, + TERM: "xterm-256color" + }; + console.log(`[ClaudeCode] HOME: ${spawnEnv.HOME}`); + + const claudeProcess = spawn(cliPath, args, { + cwd: workDir, + env: spawnEnv, + shell: false, + stdio: ["ignore", "pipe", "pipe"] + }); + + console.log(`[ClaudeCode] Process started (PID: ${claudeProcess.pid})`); + + // stdinを閉じる(claudeがstdinを待たないように) + if (claudeProcess.stdin) { + claudeProcess.stdin.end(); + } + + let stdout = ""; + let stderr = ""; + + claudeProcess.stdout.on("data", (data) => { + const chunk = data.toString(); + stdout += chunk; + // 進捗表示(最初の100文字だけ表示) + const preview = chunk.length > 100 ? chunk.slice(0, 100) + "..." : chunk; + console.log(`[ClaudeCode] stdout: ${preview.replace(/\n/g, "\\n")}`); + }); + + claudeProcess.stderr.on("data", (data) => { + const chunk = data.toString(); + stderr += chunk; + console.log(`[ClaudeCode] stderr: ${chunk.replace(/\n/g, "\\n")}`); + }); + + claudeProcess.on("close", (code) => { + console.log(`[ClaudeCode] Process exited with code: ${code}`); + if (code === 0 && stdout) { + // 出力から説明文を抽出 + const result = this._extractDescription(stdout); + console.log(`[ClaudeCode] Success! Result length: ${result.length}`); + console.log(`[ClaudeCode] Result preview: ${result.slice(0, 200)}...`); + this.dispatch(keys.claudeCodeComplete, { url, result }); + } else { + console.error("[ClaudeCode] Error:", stderr || `Exit code: ${code}`); + this.dispatch(keys.claudeCodeError, { url, error: stderr || `Exit code: ${code}` }); + } + }); + + claudeProcess.on("error", (error) => { + console.error("[ClaudeCode] Spawn error:", error); + this.dispatch(keys.claudeCodeError, { url, error: error.message }); + }); + } + + _extractDescription(output) { + // 出力からマークダウンコードブロック内の説明文を抽出 + const codeBlockMatch = output.match(/```(?:markdown)?\s*([\s\S]*?)```/); + if (codeBlockMatch) { + return codeBlockMatch[1].trim(); + } + // コードブロックがない場合はそのまま返す + return output.trim(); + } + + clearClaudeCodeResult() { + this.dispatch(keys.claudeCodeClear); + } + + insertClaudeCodeResult() { + this.dispatch(keys.claudeCodeInsert); + } } diff --git a/src/browser/Action/ServiceActionConst.js b/src/browser/Action/ServiceActionConst.js index 0495166..4afa691 100644 --- a/src/browser/Action/ServiceActionConst.js +++ b/src/browser/Action/ServiceActionConst.js @@ -14,5 +14,11 @@ export default { removeRelatedItem: Symbol("removeRelatedItem"), finishEditingRelatedItem: Symbol("finishEditingRelatedItem"), enableService: Symbol("enableService"), - disableService: Symbol("disableService") + disableService: Symbol("disableService"), + // Claude Code関連 + claudeCodeStart: Symbol("claudeCodeStart"), + claudeCodeComplete: Symbol("claudeCodeComplete"), + claudeCodeError: Symbol("claudeCodeError"), + claudeCodeClear: Symbol("claudeCodeClear"), + claudeCodeInsert: Symbol("claudeCodeInsert") }; diff --git a/src/browser/App.js b/src/browser/App.js index bcc3b39..414b5ef 100644 --- a/src/browser/App.js +++ b/src/browser/App.js @@ -10,8 +10,10 @@ import TitleInput from "./component/TitleInput"; import SubmitButton from "./component/SubmitButton"; import RelatedListBox from "./component/RelatedListBox"; import ServiceList from "./component/ServiceList"; +import ClaudeCodeButton from "./component/ClaudeCodeButton"; +import ClaudeCodePreview from "./component/ClaudeCodePreview"; import AppContext from "./AppContext"; -import serviceManger, { waitForInitialization } from "./service-instance"; +import serviceManger, { waitForInitialization, getClaudeCodeConfig } from "./service-instance"; const ipcRenderer = require("electron").ipcRenderer; const appContext = new AppContext(); @@ -20,6 +22,7 @@ class App extends React.Component { constructor(...args) { super(...args); this._TagSelect = null; + this._claudeCodeConfig = getClaudeCodeConfig(); this.state = Object.assign( { initialized: false @@ -187,6 +190,18 @@ class App extends React.Component { ServiceAction.addRelatedItem(); }; const submitPostLink = this.postLink.bind(this); + + // Claude Code関連 + const runClaudeCode = (url, config) => { + ServiceAction.runClaudeCode(url, config); + }; + const insertClaudeCodeResult = () => { + ServiceAction.insertClaudeCodeResult(); + }; + const clearClaudeCodeResult = () => { + ServiceAction.clearClaudeCodeResult(); + }; + return (
- +
+ + +
(this._TagSelect = c)} @@ -205,12 +230,18 @@ class App extends React.Component { selectTags={selectTags} selectedTags={this.state.selectedTags} /> + { if (tags.length > 0) { @@ -75,7 +82,13 @@ export default class ServiceStore extends Store { quote: "", selectedTags: [], relatedItems: [], - enabledServiceIDs: checkedServicesByDefault + enabledServiceIDs: checkedServicesByDefault, + claudeCode: { + status: "idle", + url: null, + result: null, + error: null + } }); }; this.register(keys.resetField, resetState); @@ -117,5 +130,67 @@ export default class ServiceStore extends Store { }; this.register(keys.editRelatedItem, updateRelatedItem); this.register(keys.finishEditingRelatedItem, updateRelatedItem); + + // Claude Code関連 + this.register(keys.claudeCodeStart, ({ url }) => { + this.setState({ + claudeCode: { + status: "loading", + url, + result: null, + error: null + } + }); + }); + + this.register(keys.claudeCodeComplete, ({ url, result }) => { + this.setState({ + claudeCode: { + status: "complete", + url, + result, + error: null + } + }); + }); + + this.register(keys.claudeCodeError, ({ url, error }) => { + this.setState({ + claudeCode: { + status: "error", + url, + result: null, + error + } + }); + }); + + this.register(keys.claudeCodeClear, () => { + this.setState({ + claudeCode: { + status: "idle", + url: null, + result: null, + error: null + } + }); + }); + + this.register(keys.claudeCodeInsert, () => { + const { claudeCode, comment } = this.state; + if (claudeCode.result) { + // 結果をコメントに挿入(既存のコメントがあれば改行で追加) + const newComment = comment ? `${comment}\n${claudeCode.result}` : claudeCode.result; + this.setState({ + comment: newComment, + claudeCode: { + status: "idle", + url: null, + result: null, + error: null + } + }); + } + }); } } diff --git a/src/browser/component/ClaudeCodeButton.js b/src/browser/component/ClaudeCodeButton.js new file mode 100644 index 0000000..859dd7d --- /dev/null +++ b/src/browser/component/ClaudeCodeButton.js @@ -0,0 +1,109 @@ +// LICENSE : MIT +"use strict"; +import React, { useEffect, useRef, useCallback } from "react"; + +export default function ClaudeCodeButton({ + url, + claudeCode, + runClaudeCode, + insertResult, + clearResult, + claudeCodeConfig +}) { + const prevUrlRef = useRef(url); + const debounceTimerRef = useRef(null); + + // URLが変更されたら自動でClaude Codeを実行(デバウンス付き) + useEffect(() => { + if (!claudeCodeConfig?.enabled) { + return; + } + + // URLが変更され、有効なURLの場合のみ実行 + if (url && url !== prevUrlRef.current && url.startsWith("http")) { + // 前回のタイマーをクリア + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + + // 1秒後に実行(入力中の連続変更を避ける) + debounceTimerRef.current = setTimeout(() => { + runClaudeCode(url, claudeCodeConfig); + }, 1000); + } + + prevUrlRef.current = url; + + return () => { + if (debounceTimerRef.current) { + clearTimeout(debounceTimerRef.current); + } + }; + }, [url, claudeCodeConfig, runClaudeCode]); + + const handleClick = useCallback(() => { + if (claudeCode.status === "complete") { + // 結果がある場合は挿入 + insertResult(); + } else if (claudeCode.status === "idle" || claudeCode.status === "error") { + // アイドルまたはエラー状態の場合は実行 + if (url && url.startsWith("http")) { + runClaudeCode(url, claudeCodeConfig); + } + } + }, [claudeCode.status, url, claudeCodeConfig, runClaudeCode, insertResult]); + + // 設定が無効またはCLIが設定されていない場合は表示しない + if (!claudeCodeConfig?.enabled) { + return null; + } + + // 生成中はPreviewで表示されるのでボタンは非表示 + if (claudeCode.status === "loading") { + return null; + } + + // Claudeアイコン(シンプル版) + const ClaudeIcon = () => ( + + + + ); + + const getButtonContent = () => { + switch (claudeCode.status) { + case "complete": + return ( + + ✓ + + ); + case "error": + return ( + + ! + + ); + default: + return ; + } + }; + + return ( + + ); +} diff --git a/src/browser/component/ClaudeCodePreview.js b/src/browser/component/ClaudeCodePreview.js new file mode 100644 index 0000000..75eb1f3 --- /dev/null +++ b/src/browser/component/ClaudeCodePreview.js @@ -0,0 +1,42 @@ +// LICENSE : MIT +"use strict"; +import React from "react"; + +export default function ClaudeCodePreview({ claudeCode, insertResult, clearResult }) { + if (claudeCode.status === "idle" || claudeCode.status === "loading") { + return null; + } + + if (claudeCode.status === "error") { + return ( +
+
+ エラー + +
+
{claudeCode.error}
+
+ ); + } + + if (claudeCode.status === "complete" && claudeCode.result) { + return ( +
+
+ AI生成結果 + Cmd+Shift+J で挿入 + +
+
+ {claudeCode.result} +
+
+ ); + } + + return null; +} diff --git a/src/browser/component/Editor.js b/src/browser/component/Editor.js index 75a050b..0bdf6cb 100644 --- a/src/browser/component/Editor.js +++ b/src/browser/component/Editor.js @@ -58,7 +58,7 @@ const textlintLinter = createTextlintLinter(); const Combokeys = require("combokeys"); -export default function Editor({ value, onChange, onSubmit, services, toggleServiceAtIndex }) { +export default function Editor({ value, onChange, onSubmit, services, toggleServiceAtIndex, onInsertClaudeCode }) { const combokeysRef = useRef(null); useEffect(() => { @@ -78,12 +78,19 @@ export default function Editor({ value, onChange, onSubmit, services, toggleServ toggleServiceAtIndex(services.length - 1); }); + // Cmd+Shift+J でClaude Codeの結果を挿入 + combokeysRef.current.bindGlobal(`command+shift+j`, () => { + if (onInsertClaudeCode) { + onInsertClaudeCode(); + } + }); + return () => { if (combokeysRef.current) { combokeysRef.current.detach(); } }; - }, [services.length, toggleServiceAtIndex, onSubmit]); // Ensure toggleServiceAtIndex and onSubmit are memoized in the parent component + }, [services.length, toggleServiceAtIndex, onSubmit, onInsertClaudeCode]); // Ensure toggleServiceAtIndex and onSubmit are memoized in the parent component // 最小高さを設定するテーマ const minHeightTheme = EditorView.theme({ diff --git a/src/browser/service-instance.js b/src/browser/service-instance.js index 31917d8..9545959 100644 --- a/src/browser/service-instance.js +++ b/src/browser/service-instance.js @@ -75,5 +75,19 @@ export async function waitForInitialization() { return initializeManager(); } +// Claude Code設定を取得 +export function getClaudeCodeConfig() { + try { + if (process.env.PLAYWRIGHT_TEST === "1" || process.title?.includes("playwright")) { + return { enabled: false }; + } + const serviceModule = notBundledRequire("../service.js"); + return serviceModule.claudeCodeConfig || { enabled: false }; + } catch (error) { + console.error("Failed to load Claude Code config:", error); + return { enabled: false }; + } +} + // デフォルトエクスポートはmanagerのままだが、使用前に初期化が必要 export default manager; From 22ce5519c242d6af70797bc7428e4bc7c348353e Mon Sep 17 00:00:00 2001 From: azu Date: Wed, 14 Jan 2026 21:31:22 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix:=20AI=E3=83=9C=E3=82=BF=E3=83=B3?= =?UTF-8?q?=E3=81=AEloading=E8=A1=A8=E7=A4=BA=E3=82=92=E5=BE=A9=E5=85=83?= =?UTF-8?q?=E3=81=97=E3=83=86=E3=82=AD=E3=82=B9=E3=83=88=E8=A1=A8=E7=A4=BA?= =?UTF-8?q?=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.5 --- src/browser/component/ClaudeCodeButton.js | 25 +++++++++-------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/src/browser/component/ClaudeCodeButton.js b/src/browser/component/ClaudeCodeButton.js index 859dd7d..edae117 100644 --- a/src/browser/component/ClaudeCodeButton.js +++ b/src/browser/component/ClaudeCodeButton.js @@ -58,34 +58,29 @@ export default function ClaudeCodeButton({ return null; } - // 生成中はPreviewで表示されるのでボタンは非表示 - if (claudeCode.status === "loading") { - return null; - } - - // Claudeアイコン(シンプル版) - const ClaudeIcon = () => ( - - - - ); - const getButtonContent = () => { switch (claudeCode.status) { + case "loading": + return ( + + + AI + + ); case "complete": return ( - ✓ + AI ✓ ); case "error": return ( - ! + AI ! ); default: - return ; + return AI; } }; From dd96880b60e48aed55c717d643f43a55c146b976 Mon Sep 17 00:00:00 2001 From: azu Date: Wed, 14 Jan 2026 21:44:38 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20Claude=20Code=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E3=81=AE=E6=8C=BF=E5=85=A5=E3=82=92=E8=BF=BD=E8=A8=98=E3=81=8B?= =?UTF-8?q?=E3=82=89=E5=85=A5=E3=82=8C=E6=9B=BF=E3=81=88=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit コメント欄への挿入時に既存テキストに追記するのではなく、 生成結果で全体を置き換えるように修正。 Co-Authored-By: Claude Opus 4.5 --- src/browser/Store/ServiceStore.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/browser/Store/ServiceStore.js b/src/browser/Store/ServiceStore.js index 4f98716..9920950 100644 --- a/src/browser/Store/ServiceStore.js +++ b/src/browser/Store/ServiceStore.js @@ -177,12 +177,11 @@ export default class ServiceStore extends Store { }); this.register(keys.claudeCodeInsert, () => { - const { claudeCode, comment } = this.state; + const { claudeCode } = this.state; if (claudeCode.result) { - // 結果をコメントに挿入(既存のコメントがあれば改行で追加) - const newComment = comment ? `${comment}\n${claudeCode.result}` : claudeCode.result; + // 結果でコメントを入れ替え this.setState({ - comment: newComment, + comment: claudeCode.result, claudeCode: { status: "idle", url: null, From a8fbbdf4b0a16efd510ff9cfe28e15991be370af Mon Sep 17 00:00:00 2001 From: azu Date: Wed, 14 Jan 2026 23:23:09 +0900 Subject: [PATCH 4/5] =?UTF-8?q?refactor:=20Claude=20Code=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=82=B3=E3=83=BC=E3=83=89=E3=82=92=E7=B0=A1=E6=BD=94=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - requireをimportに統一 - デバッグログを削除 - _extractDescriptionをインライン化 - 未使用のpath importを削除 Co-Authored-By: Claude Opus 4.5 --- src/browser/Action/ServiceAction.js | 114 +++++----------------------- 1 file changed, 19 insertions(+), 95 deletions(-) diff --git a/src/browser/Action/ServiceAction.js b/src/browser/Action/ServiceAction.js index be53bc2..da625d0 100644 --- a/src/browser/Action/ServiceAction.js +++ b/src/browser/Action/ServiceAction.js @@ -8,10 +8,8 @@ import notie from "notie"; import { show as LoadingShow, dismiss as LoadingDismiss } from "../view-util/Loading"; import RelatedItemModel from "../models/RelatedItemModel"; import serviceInstance from "../service-instance"; - -const { spawn } = require("child_process"); -const fs = require("fs"); -const path = require("path"); +import { spawn } from "child_process"; +import fs from "fs"; export default class ServiceAction extends Action { fetchTags(service) { @@ -198,126 +196,52 @@ export default class ServiceAction extends Action { // Claude Code関連アクション runClaudeCode(url, config) { - if (!config || !config.enabled) { - console.log("[ClaudeCode] Disabled or no config"); - return; - } - - // CLIが存在するかチェック - const cliPath = config.cliPath; - if (!fs.existsSync(cliPath)) { - console.warn(`[ClaudeCode] CLI not found at ${cliPath}`); - return; - } - - console.log(`[ClaudeCode] Starting for URL: ${url}`); - console.log(`[ClaudeCode] CLI: ${cliPath}`); - console.log(`[ClaudeCode] WorkDir: ${config.workDir}`); - - const workDir = config.workDir; - - // 作業ディレクトリの存在確認 - if (!fs.existsSync(workDir)) { - console.error(`[ClaudeCode] WorkDir does not exist: ${workDir}`); - this.dispatch(keys.claudeCodeError, { url, error: `WorkDir does not exist: ${workDir}` }); + if (!config?.enabled) return; + if (!fs.existsSync(config.cliPath)) return; + if (!fs.existsSync(config.workDir)) { + this.dispatch(keys.claudeCodeError, { url, error: `WorkDir not found: ${config.workDir}` }); return; } - console.log(`[ClaudeCode] WorkDir exists: OK`); this.dispatch(keys.claudeCodeStart, { url }); - // 設定からプロンプトを使用 - const prompt = `${config.prompt}\n\nURL: ${url}`; - - // Claude Code CLIを--print付きで実行(ワンショットモード) - // ツール許可オプションを追加 const args = []; - - // MCP設定がある場合は追加 if (config.mcpConfig) { - const mcpJson = JSON.stringify(config.mcpConfig); - args.push("--mcp-config", mcpJson); - console.log(`[ClaudeCode] MCP config: ${mcpJson.slice(0, 100)}...`); + args.push("--mcp-config", JSON.stringify(config.mcpConfig)); } - - args.push("--print", "--dangerously-skip-permissions", prompt); - console.log( - `[ClaudeCode] Spawning: ${cliPath} ${ - config.mcpConfig ? "--mcp-config " : "" - }--print --dangerously-skip-permissions ` - ); - console.log(`[ClaudeCode] Prompt length: ${prompt.length} chars`); - console.log(`[ClaudeCode] Prompt (last 200 chars): ...${prompt.slice(-200)}`); - - const spawnEnv = { - ...process.env, - HOME: process.env.HOME, - PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + process.env.PATH, - TERM: "xterm-256color" - }; - console.log(`[ClaudeCode] HOME: ${spawnEnv.HOME}`); - - const claudeProcess = spawn(cliPath, args, { - cwd: workDir, - env: spawnEnv, + args.push("--print", "--dangerously-skip-permissions", `${config.prompt}\n\nURL: ${url}`); + + const claudeProcess = spawn(config.cliPath, args, { + cwd: config.workDir, + env: { + ...process.env, + PATH: "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:" + process.env.PATH + }, shell: false, stdio: ["ignore", "pipe", "pipe"] }); - console.log(`[ClaudeCode] Process started (PID: ${claudeProcess.pid})`); - - // stdinを閉じる(claudeがstdinを待たないように) - if (claudeProcess.stdin) { - claudeProcess.stdin.end(); - } - let stdout = ""; let stderr = ""; - claudeProcess.stdout.on("data", (data) => { - const chunk = data.toString(); - stdout += chunk; - // 進捗表示(最初の100文字だけ表示) - const preview = chunk.length > 100 ? chunk.slice(0, 100) + "..." : chunk; - console.log(`[ClaudeCode] stdout: ${preview.replace(/\n/g, "\\n")}`); - }); - - claudeProcess.stderr.on("data", (data) => { - const chunk = data.toString(); - stderr += chunk; - console.log(`[ClaudeCode] stderr: ${chunk.replace(/\n/g, "\\n")}`); - }); + claudeProcess.stdout.on("data", (data) => (stdout += data.toString())); + claudeProcess.stderr.on("data", (data) => (stderr += data.toString())); claudeProcess.on("close", (code) => { - console.log(`[ClaudeCode] Process exited with code: ${code}`); if (code === 0 && stdout) { - // 出力から説明文を抽出 - const result = this._extractDescription(stdout); - console.log(`[ClaudeCode] Success! Result length: ${result.length}`); - console.log(`[ClaudeCode] Result preview: ${result.slice(0, 200)}...`); + const match = stdout.match(/```(?:markdown)?\s*([\s\S]*?)```/); + const result = match ? match[1].trim() : stdout.trim(); this.dispatch(keys.claudeCodeComplete, { url, result }); } else { - console.error("[ClaudeCode] Error:", stderr || `Exit code: ${code}`); this.dispatch(keys.claudeCodeError, { url, error: stderr || `Exit code: ${code}` }); } }); claudeProcess.on("error", (error) => { - console.error("[ClaudeCode] Spawn error:", error); this.dispatch(keys.claudeCodeError, { url, error: error.message }); }); } - _extractDescription(output) { - // 出力からマークダウンコードブロック内の説明文を抽出 - const codeBlockMatch = output.match(/```(?:markdown)?\s*([\s\S]*?)```/); - if (codeBlockMatch) { - return codeBlockMatch[1].trim(); - } - // コードブロックがない場合はそのまま返す - return output.trim(); - } - clearClaudeCodeResult() { this.dispatch(keys.claudeCodeClear); } From 8df1f16901c0d572f10d5526315a133ec388987e Mon Sep 17 00:00:00 2001 From: azu Date: Wed, 14 Jan 2026 23:52:38 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20fetchContent=E3=81=8Cundefined?= =?UTF-8?q?=E3=82=92=E8=BF=94=E3=81=99=E5=A0=B4=E5=90=88=E3=81=AE=E3=82=A8?= =?UTF-8?q?=E3=83=A9=E3=83=BC=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getContentがundefinedを返す場合にdestructureエラーが 発生していた問題を修正。 Co-Authored-By: Claude Opus 4.5 --- src/browser/App.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/browser/App.js b/src/browser/App.js index 414b5ef..d17d140 100644 --- a/src/browser/App.js +++ b/src/browser/App.js @@ -73,7 +73,9 @@ class App extends React.Component { const service = serviceManger.getTagService(); if (service && state.selectedTags.length === 0 && state.comment.length === 0) { appContext.ServiceAction.fetchContent(service, URL) - .then(({ comment, tags, relatedItems }) => { + .then((result) => { + if (!result) return; + const { comment, tags, relatedItems } = result; if (comment) { appContext.ServiceAction.updateComment(comment); }