hwpx-bridge 0.3.1 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,29 +9,40 @@ AI 에이전트가 한글 문서(HWP·HWPX)를 다룰 수 있게 해 주는 **MC
9
9
 
10
10
  ## 설치 — 한 줄
11
11
 
12
- [Node.js](https://nodejs.org) 20 이상이 있으면(kordoc과 같은 조건) 아무 터미널에서:
13
-
14
12
  ```
15
13
  npx -y hwpx-bridge setup
16
14
  ```
17
15
 
18
- 등록할 앱을 고르면 끝입니다: Claude 앱, Claude Code, Codex, Cursor, Gemini CLI, Windsurf. 앱을 한 번 다시 시작하면 도구가 나타납니다.
16
+ 터미널(PowerShell 등)에 입력하든, Codex·Claude Code에 "이 명령 실행해줘"라고 하든 결과는 같습니다. **이 PC에 설치된 AI 앱을 모두 찾아 한 번에 등록**합니다. 묻는 것은 없습니다.
17
+
18
+ | 찾는 앱 | 등록 위치 |
19
+ | --- | --- |
20
+ | Claude 앱 | `claude_desktop_config.json` |
21
+ | Claude Code | `claude mcp add --scope user` |
22
+ | Codex (ChatGPT 데스크톱 앱·CLI·IDE 확장이 함께 쓰는 설정) | `~/.codex/config.toml` |
23
+ | Cursor, Gemini CLI, Windsurf | 각 앱의 MCP 설정 파일 |
24
+
25
+ 등록한 앱을 한 번 완전히 종료했다가 다시 열면, 그 앱의 **모든 대화**에서 도구가 보입니다. 작업 폴더와 상관없이 PC 안의 어느 파일이든 경로로 다룰 수 있습니다. 나중에 새 AI 앱을 설치했다면 같은 명령을 한 번 더 실행하면 됩니다. 기존 설정은 백업(`.bak-…`)을 남기고 유지됩니다.
19
26
 
20
- 번호를 고르지 않고 바로 등록하려면:
27
+ **Node.js가 없는 Windows PC**: `npx`가 없다는 오류가 나면 PowerShell에 아래 한 줄을 넣으세요. Node.js LTS를 설치한 뒤(winget이 있으면 winget, 없으면 nodejs.org 공식 파일을 SHA-256 확인 후 사용자 폴더에 설치 — 관리자 권한 불필요) 위 명령을 대신 실행합니다.
21
28
 
22
29
  ```
23
- npx -y hwpx-bridge setup --client claude-desktop,claude-code,codex
30
+ irm https://raw.githubusercontent.com/ThisIsMarkive/foryourdocument/main/install.ps1 | iex
24
31
  ```
25
32
 
26
- 직접 넣는 경우 (`npx -y hwpx-bridge config`가 출력하는 것과 같음):
33
+ 일부 앱에만 등록하려면 `npx -y hwpx-bridge setup --client claude-desktop,codex` (값: `claude-desktop, claude-code, codex, cursor, gemini, windsurf`).
34
+
35
+ 웹·모바일(claude.ai 웹, ChatGPT 웹, 휴대폰 앱)은 내 PC의 프로그램을 실행할 수 없어 지원되지 않습니다.
36
+
37
+ 설정 파일에 직접 넣는 경우 (`npx -y hwpx-bridge config`가 출력하는 것과 같음):
27
38
 
28
39
  ```json
29
- { "mcpServers": { "hwpx-bridge": { "command": "npx", "args": ["-y", "hwpx-bridge@0.3.1", "mcp"] } } }
40
+ { "mcpServers": { "hwpx-bridge": { "command": "npx", "args": ["-y", "hwpx-bridge@0.3.4", "mcp"] } } }
30
41
  ```
31
42
 
32
- Windows의 Claude 앱은 `"command": "cmd", "args": ["/c", "npx", "-y", "hwpx-bridge@0.3.1", "mcp"]` 형식이 필요하며, `setup`이 자동으로 맞춥니다. 처음 실행할 때 엔진(약 100 MB)을 내려받으므로, `setup`을 먼저 실행해 두면 앱 첫 실행이 빠릅니다.
43
+ Windows의 Claude 앱은 `"command": "cmd", "args": ["/c", "npx", "-y", "hwpx-bridge@0.3.4", "mcp"]` 형식이 필요하며, `setup`이 자동으로 맞춥니다. 처음 실행할 때 엔진(약 100 MB)을 내려받으므로, `setup`을 먼저 실행해 두면 앱 첫 실행이 빠릅니다.
33
44
 
34
- Claude Code·Codex 플러그인 마켓플레이스(`ThisIsMarkive/foryourdocument`)로 설치해도 같은 서버(`npx -y hwpx-bridge@0.3.1 mcp`)와 사용 안내 스킬이 등록됩니다.
45
+ Claude Code·Codex 플러그인 마켓플레이스(`ThisIsMarkive/foryourdocument`)로 설치해도 같은 서버(`npx -y hwpx-bridge@0.3.4 mcp`)와 사용 안내 스킬이 등록됩니다.
35
46
 
36
47
  확인: `npx -y hwpx-bridge doctor`
37
48
 
@@ -79,7 +90,7 @@ Filee는 소스를 빌드하지 않고 **공식 릴리스의 파일을 그대로
79
90
  ## 배포 (관리자용)
80
91
 
81
92
  1. npm 계정을 만들고(2단계 인증 필수) Granular Access Token을 만듭니다: 권한 Read and write, 대상 All packages, **Bypass 2FA** 체크, 만료 최대 90일. GitHub 저장소 Secret `NPM_TOKEN`으로 등록합니다. 첫 배포 뒤에는 각 패키지 설정에서 Trusted Publishing(GitHub Actions)을 켜면 토큰 갱신이 필요 없습니다.
82
- 2. 세 `package.json`과 플러그인 매니페스트의 버전을 올리고, `vX.Y.Z` 태그를 푸시하거나 GitHub Releases에서 같은 태그로 릴리스를 만듭니다.
93
+ 2. 세 `package.json`과 플러그인 매니페스트의 버전을 올리고 GitHub Releases에서 `vX.Y.Z` 태그로 릴리스를 만듭니다(태그만 푸시하면 배포되지 않습니다).
83
94
  3. GitHub Actions가 Filee 준비 → 패키지 생성 → Windows·Linux에서 실제 설치·변환 테스트 → npm 배포를 순서대로 합니다.
84
95
 
85
96
  로컬에서 확인하려면: `node npm/scripts/prepare-filee.mjs all` 후 각 폴더에서 `npm pack`.
@@ -7,8 +7,8 @@ const [command = 'help', ...rest] = process.argv.slice(2);
7
7
 
8
8
  const HELP = `hwpx-bridge ${pkg.version} — kordoc + Filee document conversion as one MCP server
9
9
 
10
- npx -y hwpx-bridge setup AI 앱(Claude, Claude Code, Codex, Cursor ...)에 등록
11
- npx -y hwpx-bridge setup --client claude-desktop,codex
10
+ npx -y hwpx-bridge setup 이 PC의 AI 앱(Claude 앱, Claude Code, Codex, Cursor ...) 모두에 등록
11
+ npx -y hwpx-bridge setup --client claude-desktop,codex 일부 앱에만 등록
12
12
  npx -y hwpx-bridge config 직접 붙여 넣을 MCP 설정 JSON 출력
13
13
  npx -y hwpx-bridge doctor kordoc·Filee 실행 확인
14
14
  npx -y hwpx-bridge mcp MCP 서버 실행 (앱이 이 명령을 실행합니다)
package/lib/filee.mjs CHANGED
@@ -21,7 +21,10 @@ export function locateFilee() {
21
21
  let root;
22
22
  try { root = path.dirname(require.resolve(`${PLATFORM_PACKAGE}/package.json`)); }
23
23
  catch {
24
- return { error: `Filee is not available for ${process.platform}-${process.arch}. Supported: Windows x64, Linux x64. kordoc tools still work.` };
24
+ const supported = ['win32-x64', 'linux-x64'].includes(`${process.platform}-${process.arch}`);
25
+ return { error: supported
26
+ ? `${PLATFORM_PACKAGE} is not installed (npm skipped it). Clear the npx cache (%LOCALAPPDATA%\\npm-cache\\_npx on Windows, ~/.npm/_npx elsewhere) and run setup again. kordoc tools still work.`
27
+ : `Filee conversion is not available on ${process.platform}-${process.arch} (Windows x64 and Linux x64 only). kordoc tools still work.` };
25
28
  }
26
29
  const cli = path.join(root, 'filee', process.platform === 'win32' ? 'filee-cli.exe' : 'filee-cli');
27
30
  if (!fss.existsSync(cli)) return { error: `${PLATFORM_PACKAGE} is installed but ${cli} is missing.` };
package/lib/setup.mjs CHANGED
@@ -1,13 +1,14 @@
1
- // `npx -y hwpx-bridge setup` — register the MCP server in AI clients.
2
- // Same approach as kordoc's src/setup.ts (MIT, chrisryugj): JSON config files are merged in place,
3
- // Claude Code and Codex are registered through their own CLIs. Nothing else on the computer is changed.
1
+ // `npx -y hwpx-bridge setup` — register the MCP server in every AI app found on this computer.
2
+ //
3
+ // No questions: wherever the command runs (a terminal, Codex, Claude Code ...), every installed app gets
4
+ // the same entry. `--client a,b` limits it to some apps. Config files are merged in place with a backup;
5
+ // Claude Code is registered through its own CLI. Nothing else on the computer is changed.
4
6
  import fs from 'node:fs/promises';
5
7
  import fss from 'node:fs';
6
8
  import os from 'node:os';
7
9
  import path from 'node:path';
8
- import { execFile } from 'node:child_process';
10
+ import { execFile, spawnSync } from 'node:child_process';
9
11
  import { promisify } from 'node:util';
10
- import { createInterface } from 'node:readline/promises';
11
12
  import { createRequire } from 'node:module';
12
13
 
13
14
  const run = promisify(execFile);
@@ -15,77 +16,122 @@ const require = createRequire(import.meta.url);
15
16
  const pkg = require('../package.json');
16
17
  const NAME = 'hwpx-bridge';
17
18
  const isWin = process.platform === 'win32';
19
+ const home = () => os.homedir();
18
20
 
19
21
  /** Pinned so a client never silently runs a newer (or hijacked) npm release. */
20
22
  export function packageSpec(version = pkg.version) {
21
23
  return /^\d+\.\d+\.\d+$/.test(version) ? `${NAME}@${version}` : NAME;
22
24
  }
23
25
 
24
- /** Claude Desktop on Windows cannot start npx.cmd directly, hence `cmd /c`. */
26
+ /** Apps on Windows cannot start npx.cmd directly, hence `cmd /c`. */
25
27
  export function serverEntry() {
26
28
  return isWin
27
29
  ? { command: 'cmd', args: ['/c', 'npx', '-y', packageSpec(), 'mcp'] }
28
30
  : { command: 'npx', args: ['-y', packageSpec(), 'mcp'] };
29
31
  }
30
32
 
33
+ function onPath(command) {
34
+ const result = spawnSync(isWin ? 'where' : 'which', [command], { stdio: 'ignore', windowsHide: true });
35
+ return result.status === 0;
36
+ }
37
+
38
+ const exists = file => fss.existsSync(file);
39
+
31
40
  function claudeDesktopConfig() {
32
- const home = os.homedir();
33
41
  if (isWin) {
34
- const appData = process.env.APPDATA ?? path.join(home, 'AppData/Roaming');
35
- const localAppData = process.env.LOCALAPPDATA ?? path.join(home, 'AppData/Local');
42
+ const appData = process.env.APPDATA ?? path.join(home(), 'AppData/Roaming');
43
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(home(), 'AppData/Local');
36
44
  // The Microsoft Store build of Claude reads a package-local copy of %APPDATA%.
37
45
  const store = path.join(localAppData, 'Packages/Claude_pzs8sxrjxfjjc/LocalCache/Roaming/Claude');
38
- return path.join(fss.existsSync(store) ? store : path.join(appData, 'Claude'), 'claude_desktop_config.json');
46
+ return path.join(exists(store) ? store : path.join(appData, 'Claude'), 'claude_desktop_config.json');
39
47
  }
40
- if (process.platform === 'darwin') return path.join(home, 'Library/Application Support/Claude/claude_desktop_config.json');
41
- return path.join(home, '.config/Claude/claude_desktop_config.json');
48
+ if (process.platform === 'darwin') return path.join(home(), 'Library/Application Support/Claude/claude_desktop_config.json');
49
+ return path.join(home(), '.config/Claude/claude_desktop_config.json');
50
+ }
51
+
52
+ function claudeDesktopInstalled() {
53
+ if (exists(path.dirname(claudeDesktopConfig()))) return true;
54
+ if (isWin) return exists(path.join(process.env.LOCALAPPDATA ?? path.join(home(), 'AppData/Local'), 'AnthropicClaude'));
55
+ if (process.platform === 'darwin') return exists('/Applications/Claude.app');
56
+ return false;
42
57
  }
43
58
 
59
+ const codexHome = () => process.env.CODEX_HOME || path.join(home(), '.codex');
60
+
44
61
  export const CLIENTS = [
45
- { id: 'claude-desktop', label: 'Claude 앱 (Claude Desktop)', kind: 'json', file: claudeDesktopConfig },
46
- { id: 'claude-code', label: 'Claude Code (사용자 전체)', kind: 'cli', cli: 'claude' },
47
- { id: 'codex', label: 'Codex (ChatGPT)', kind: 'cli', cli: 'codex' },
48
- { id: 'cursor', label: 'Cursor', kind: 'json', file: () => path.join(os.homedir(), '.cursor/mcp.json') },
49
- { id: 'gemini', label: 'Gemini CLI', kind: 'json', file: () => path.join(os.homedir(), '.gemini/settings.json') },
50
- { id: 'windsurf', label: 'Windsurf', kind: 'json', file: () => path.join(os.homedir(), '.codeium/windsurf/mcp_config.json') }
62
+ { id: 'claude-desktop', label: 'Claude 앱', found: claudeDesktopInstalled, register: () => registerJson(claudeDesktopConfig()) },
63
+ { id: 'claude-code', label: 'Claude Code', found: () => onPath('claude'), register: registerClaudeCode },
64
+ { id: 'codex', label: 'Codex (ChatGPT 데스크톱·CLI·IDE)', found: () => exists(codexHome()) || onPath('codex'), register: registerCodex },
65
+ { id: 'cursor', label: 'Cursor', found: () => exists(path.join(home(), '.cursor')), register: () => registerJson(path.join(home(), '.cursor/mcp.json')) },
66
+ { id: 'gemini', label: 'Gemini CLI', found: () => exists(path.join(home(), '.gemini')), register: () => registerJson(path.join(home(), '.gemini/settings.json')) },
67
+ { id: 'windsurf', label: 'Windsurf', found: () => exists(path.join(home(), '.codeium/windsurf')), register: () => registerJson(path.join(home(), '.codeium/windsurf/mcp_config.json')) }
51
68
  ];
52
69
 
70
+ async function backup(file) {
71
+ if (exists(file)) await fs.copyFile(file, `${file}.bak-${Date.now()}`);
72
+ }
73
+
53
74
  async function registerJson(file) {
54
75
  let config = {};
55
- if (fss.existsSync(file)) {
76
+ if (exists(file)) {
56
77
  const raw = (await fs.readFile(file, 'utf8')).replace(/^/, '');
57
78
  config = raw.trim() ? JSON.parse(raw) : {};
58
79
  }
59
80
  if (typeof config !== 'object' || config === null || Array.isArray(config)) throw new Error(`설정 파일 형식이 올바르지 않아 그대로 두었습니다: ${file}`);
60
81
  const entry = serverEntry();
61
82
  if (JSON.stringify(config.mcpServers?.[NAME]) === JSON.stringify(entry)) return `이미 등록됨 (${file})`;
62
- if (fss.existsSync(file)) await fs.copyFile(file, `${file}.bak-${Date.now()}`);
83
+ await backup(file);
63
84
  config.mcpServers = { ...(config.mcpServers ?? {}), [NAME]: entry };
64
85
  await fs.mkdir(path.dirname(file), { recursive: true });
65
86
  await fs.writeFile(file, JSON.stringify(config, null, 2) + '\n', 'utf8');
66
87
  return `등록 완료 (${file})`;
67
88
  }
68
89
 
69
- async function registerCli(cli) {
70
- const options = { windowsHide: true, timeout: 60_000, shell: isWin }; // claude/codex are .cmd shims on Windows
90
+ /** Codex keeps MCP servers in config.toml, shared by the Codex app, CLI and IDE extension. */
91
+ export function codexBlock() {
71
92
  const entry = serverEntry();
72
- const scope = cli === 'claude' ? ['--scope', 'user'] : [];
73
- // Replace an earlier hwpx-bridge registration (e.g. an older pinned version).
74
- await run(cli, ['mcp', 'remove', ...scope, NAME], options).catch(() => {});
75
- await run(cli, ['mcp', 'add', ...scope, NAME, '--', entry.command, ...entry.args], options);
76
- return `등록 완료 (${cli} mcp add)`;
93
+ return [
94
+ `[mcp_servers.${NAME}]`,
95
+ `command = ${JSON.stringify(entry.command)}`,
96
+ `args = [${entry.args.map(a => JSON.stringify(a)).join(', ')}]`,
97
+ 'startup_timeout_sec = 180 # first start downloads the engines',
98
+ 'tool_timeout_sec = 1800 # OCR model download, long conversions',
99
+ ''
100
+ ].join('\n');
77
101
  }
78
102
 
79
- async function choose() {
80
- const rl = createInterface({ input: process.stdin, output: process.stdout });
81
- try {
82
- console.log('\nhwpx-bridge를 등록할 AI 앱을 고르세요 (여러 개는 쉼표로, 예: 1,3)\n');
83
- CLIENTS.forEach((client, i) => console.log(` ${i + 1}. ${client.label}`));
84
- const answer = await rl.question('\n번호: ');
85
- const picked = answer.split(/[,\s]+/).filter(Boolean).map(n => CLIENTS[Number(n) - 1]);
86
- if (!picked.length || picked.some(c => !c)) throw new Error('올바른 번호를 입력하세요.');
87
- return picked;
88
- } finally { rl.close(); }
103
+ /** Replace an existing [mcp_servers.hwpx-bridge] table (and its sub-tables) or append one. */
104
+ export function mergeCodexToml(text) {
105
+ const header = /^\s*\[\s*mcp_servers\s*\.\s*(?:"hwpx-bridge"|hwpx-bridge)\s*(?:\.[^\]]*)?\]\s*(#.*)?$/;
106
+ const lines = text.replace(/^/, '').split(/\r?\n/);
107
+ const kept = [];
108
+ let skipping = false;
109
+ for (const line of lines) {
110
+ if (/^\s*\[/.test(line)) skipping = header.test(line);
111
+ if (!skipping) kept.push(line);
112
+ }
113
+ const body = kept.join('\n').replace(/\s+$/, '');
114
+ return (body ? `${body}\n\n` : '') + codexBlock();
115
+ }
116
+
117
+ async function registerCodex() {
118
+ const file = path.join(codexHome(), 'config.toml');
119
+ const before = exists(file) ? await fs.readFile(file, 'utf8') : '';
120
+ const after = mergeCodexToml(before);
121
+ if (before.replace(/\r\n/g, '\n') === after) return `이미 등록됨 (${file})`;
122
+ await backup(file);
123
+ await fs.mkdir(path.dirname(file), { recursive: true });
124
+ await fs.writeFile(file, after, 'utf8');
125
+ return `등록 완료 (${file})`;
126
+ }
127
+
128
+ async function registerClaudeCode() {
129
+ const options = { windowsHide: true, timeout: 60_000, shell: isWin }; // claude is a .cmd shim on Windows
130
+ const entry = serverEntry();
131
+ // Replace an earlier registration (e.g. an older pinned version).
132
+ await run('claude', ['mcp', 'remove', '--scope', 'user', NAME], options).catch(() => {});
133
+ await run('claude', ['mcp', 'add', '--scope', 'user', NAME, '--', entry.command, ...entry.args], options);
134
+ return '등록 완료 (claude mcp add --scope user)';
89
135
  }
90
136
 
91
137
  export async function runSetup(argv) {
@@ -95,25 +141,33 @@ export async function runSetup(argv) {
95
141
  const ids = (argv[flag + 1] ?? '').split(',').filter(Boolean);
96
142
  clients = ids.map(id => CLIENTS.find(c => c.id === id));
97
143
  if (!ids.length || clients.some(c => !c)) throw new Error(`--client 값: ${CLIENTS.map(c => c.id).join(', ')}`);
98
- } else if (process.stdin.isTTY) clients = await choose();
99
- else throw new Error(`대화형 터미널이 아닙니다. --client ${CLIENTS.map(c => c.id).join(',')} 중에서 지정하세요.`);
100
-
144
+ } else {
145
+ clients = CLIENTS.filter(client => client.found());
146
+ const missing = CLIENTS.filter(client => !clients.includes(client)).map(client => client.label);
147
+ console.log(`\nhwpx-bridge ${pkg.version}: 이 PC에서 찾은 AI 앱 모두에 등록합니다.`);
148
+ if (missing.length) console.log(` (설치되지 않아 건너뜀: ${missing.join(', ')})`);
149
+ if (!clients.length) {
150
+ console.log('\n등록할 AI 앱을 찾지 못했습니다. 앱을 설치한 뒤 다시 실행하거나, 아래 설정을 앱에 직접 넣으세요.\n');
151
+ printConfig();
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+ }
156
+ console.log('');
101
157
  let failed = false;
102
158
  for (const client of clients) {
103
159
  try {
104
- const message = client.kind === 'json' ? await registerJson(client.file()) : await registerCli(client.cli);
105
- console.log(` + ${client.label}: ${message}`);
160
+ console.log(` + ${client.label}: ${await client.register()}`);
106
161
  } catch (error) {
107
162
  failed = true;
108
- const hint = error.code === 'ENOENT' ? `${client.cli} 명령을 찾을 수 없습니다.` : error.message;
109
- console.log(` x ${client.label}: ${hint}`);
163
+ console.log(` x ${client.label}: ${error.code === 'ENOENT' ? '명령을 찾을 수 없습니다.' : error.message.split('\n')[0]}`);
110
164
  }
111
165
  }
112
- console.log(`\n앱을 다시 시작하면 'hwpx-bridge' 도구가 나타납니다 (kordoc 도구 + filee_convert).`);
113
- console.log(`직접 설정하려면: npx -y ${packageSpec()} config`);
166
+ console.log('\n등록한 앱을 완전히 종료했다가 다시 열면 hwpx-bridge 도구(kordoc 전체 + filee_convert)가 모든 대화에서 보입니다.');
114
167
  if (failed) process.exitCode = 1;
115
168
  }
116
169
 
117
170
  export function printConfig() {
118
171
  console.log(JSON.stringify({ mcpServers: { [NAME]: serverEntry() } }, null, 2));
172
+ console.log(`\nCodex (~/.codex/config.toml):\n\n${codexBlock()}`);
119
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hwpx-bridge",
3
- "version": "0.3.1",
3
+ "version": "0.3.4",
4
4
  "description": "One MCP server for Korean documents: every kordoc tool (read/write/edit HWP·HWPX·PDF, OCR) plus Filee document conversion (DOCX→HWPX with formatting). One-line setup for Claude, Codex, Cursor and more.",
5
5
  "type": "module",
6
6
  "bin": { "hwpx-bridge": "bin/hwpx-bridge.mjs" },
@@ -11,8 +11,8 @@
11
11
  "kordoc": "4.24.62"
12
12
  },
13
13
  "optionalDependencies": {
14
- "hwpx-bridge-filee-win32-x64": "0.3.1",
15
- "hwpx-bridge-filee-linux-x64": "0.3.1"
14
+ "hwpx-bridge-filee-win32-x64": "0.3.4",
15
+ "hwpx-bridge-filee-linux-x64": "0.3.4"
16
16
  },
17
17
  "keywords": ["hwp", "hwpx", "docx", "mcp", "kordoc", "filee", "korean", "ocr", "claude", "codex"],
18
18
  "license": "MIT",