Skip to content

Commit 02d3952

Browse files
committed
Add npm distribution package
1 parent f81d7b6 commit 02d3952

8 files changed

Lines changed: 419 additions & 1 deletion

File tree

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,16 @@ office-addin/node_modules/
2222
office-addin/dist/
2323

2424
# .NET
25-
bin/
25+
/bin/
2626
obj/
2727
dotnet/**/bin/
2828
dotnet/**/obj/
2929

30+
# Root npm CLI entrypoints
31+
!/bin/
32+
!/bin/word-ai*.js
33+
!/bin/word-ai-launcher.cjs
34+
3035
# Word AI local sidecars and generated verification artifacts
3136
.wordai/
3237
examples/.wordai/

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@ The Office.js taskpane is the Word session layer. It creates and lists content c
5151

5252
## Quick Start
5353

54+
No-clone npm quick check:
55+
56+
```bash
57+
npx -y word-ai --root "$PWD" doctor
58+
```
59+
60+
Run the MCP stdio server through npm:
61+
62+
```bash
63+
npm exec --yes --package word-ai -- word-ai-mcp --root "$PWD" --allow-root "$HOME/Downloads"
64+
```
65+
66+
After a global install, the same commands are available directly:
67+
68+
```bash
69+
npm install -g word-ai
70+
word-ai --root "$PWD" doctor
71+
word-ai-mcp --root "$PWD" --allow-root "$HOME/Downloads"
72+
```
73+
74+
For full local Office.js live-session editing, use the repository setup below.
75+
5476
One-command local setup:
5577

5678
```bash
@@ -177,6 +199,31 @@ startup_timeout_sec = 30
177199
PYTHONPATH = "/absolute/path/to/word-ai"
178200
```
179201

202+
For npm-based Codex setup without cloning the repo:
203+
204+
```toml
205+
[mcp_servers.word_ai]
206+
command = "npm"
207+
args = [
208+
"exec",
209+
"--yes",
210+
"--package",
211+
"word-ai",
212+
"--",
213+
"word-ai-mcp",
214+
"--root",
215+
"/absolute/path/to/workspace",
216+
"--allow-root",
217+
"/Users/you/Downloads",
218+
"--allow-root",
219+
"/Users/you/Documents"
220+
]
221+
enabled = true
222+
startup_timeout_sec = 60
223+
```
224+
225+
The first npm run creates a local Python virtual environment under the user cache and installs Word AI Python dependencies automatically. Set `WORD_AI_PYTHON=/path/to/python3.10+` if Python discovery needs help.
226+
180227
`--root` is the primary workspace for relative paths and Word AI sidecars. Repeat `--allow-root` for external document folders you want Codex to edit, such as Downloads, Documents, or a team project folder. The installer-generated `.wordai/codex-config.toml` includes common user document folders automatically.
181228

182229
Recommended approval policy for write tools:
@@ -321,6 +368,28 @@ Word AI 是一个开源 MCP Server 与 Office.js Bridge,用于安全、可审
321368

322369
## 快速开始
323370

371+
npm 免 clone 快速检查:
372+
373+
```bash
374+
npx -y word-ai --root "$PWD" doctor
375+
```
376+
377+
通过 npm 运行 MCP stdio server:
378+
379+
```bash
380+
npm exec --yes --package word-ai -- word-ai-mcp --root "$PWD" --allow-root "$HOME/Downloads"
381+
```
382+
383+
全局安装后可直接使用:
384+
385+
```bash
386+
npm install -g word-ai
387+
word-ai --root "$PWD" doctor
388+
word-ai-mcp --root "$PWD" --allow-root "$HOME/Downloads"
389+
```
390+
391+
如果需要完整 Office.js live session 编辑,再使用下面的仓库本地安装方式。
392+
324393
一键本地安装和启动:
325394

326395
```bash
@@ -446,6 +515,31 @@ startup_timeout_sec = 30
446515
PYTHONPATH = "/absolute/path/to/word-ai"
447516
```
448517

518+
如果不想 clone 仓库,也可以让 Codex 直接通过 npm 启动:
519+
520+
```toml
521+
[mcp_servers.word_ai]
522+
command = "npm"
523+
args = [
524+
"exec",
525+
"--yes",
526+
"--package",
527+
"word-ai",
528+
"--",
529+
"word-ai-mcp",
530+
"--root",
531+
"/absolute/path/to/workspace",
532+
"--allow-root",
533+
"/Users/you/Downloads",
534+
"--allow-root",
535+
"/Users/you/Documents"
536+
]
537+
enabled = true
538+
startup_timeout_sec = 60
539+
```
540+
541+
首次 npm 启动会在用户缓存目录创建 Python venv,并自动安装 Word AI Python 依赖。如需指定 Python,可设置 `WORD_AI_PYTHON=/path/to/python3.10+`
542+
449543
`--root` 是相对路径和 `.wordai` sidecar 的主工作区。需要编辑 Downloads、Documents 或团队目录中的原始 DOCX 时,重复添加 `--allow-root`。安装脚本生成的 `.wordai/codex-config.toml` 会自动加入常见用户文档目录。
450544

451545
推荐将写入类工具设置为需要审批,尤其是 `docx_apply_patchset``docx_restore_backup``docx_rollback`

bin/word-ai-http.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
require("./word-ai-launcher.cjs").main("word_ai_mcp.server_http");

bin/word-ai-launcher.cjs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
const childProcess = require("node:child_process");
5+
const fs = require("node:fs");
6+
const os = require("node:os");
7+
const path = require("node:path");
8+
9+
function packageRoot() {
10+
return fs.realpathSync(path.resolve(__dirname, ".."));
11+
}
12+
13+
function readPackage(root) {
14+
return JSON.parse(fs.readFileSync(path.join(root, "package.json"), "utf8"));
15+
}
16+
17+
function cacheRoot() {
18+
if (process.env.WORD_AI_NPM_CACHE) {
19+
return path.resolve(process.env.WORD_AI_NPM_CACHE);
20+
}
21+
if (process.platform === "win32") {
22+
return path.join(process.env.LOCALAPPDATA || os.homedir(), "word-ai", "npm");
23+
}
24+
return path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache"), "word-ai", "npm");
25+
}
26+
27+
function venvPython(venvDir) {
28+
return process.platform === "win32"
29+
? path.join(venvDir, "Scripts", "python.exe")
30+
: path.join(venvDir, "bin", "python");
31+
}
32+
33+
function sleepSync(ms) {
34+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
35+
}
36+
37+
function acquireLock(lockDir) {
38+
const start = Date.now();
39+
while (true) {
40+
try {
41+
fs.mkdirSync(lockDir, {recursive: false});
42+
fs.writeFileSync(path.join(lockDir, "pid"), `${process.pid}\n`, "utf8");
43+
return;
44+
} catch (error) {
45+
if (error && error.code !== "EEXIST") {
46+
throw error;
47+
}
48+
try {
49+
const stat = fs.statSync(lockDir);
50+
if (Date.now() - stat.mtimeMs > 10 * 60 * 1000) {
51+
fs.rmSync(lockDir, {recursive: true, force: true});
52+
continue;
53+
}
54+
} catch (_) {
55+
continue;
56+
}
57+
if (Date.now() - start > 5 * 60 * 1000) {
58+
throw new Error(`Timed out waiting for Word AI npm bootstrap lock: ${lockDir}`);
59+
}
60+
sleepSync(250);
61+
}
62+
}
63+
}
64+
65+
function releaseLock(lockDir) {
66+
fs.rmSync(lockDir, {recursive: true, force: true});
67+
}
68+
69+
function runChecked(command, args, options = {}) {
70+
const proc = childProcess.spawnSync(command, args, {
71+
stdio: options.stdio || "inherit",
72+
env: options.env || process.env,
73+
cwd: options.cwd || process.cwd(),
74+
shell: false
75+
});
76+
if (proc.error) {
77+
throw proc.error;
78+
}
79+
if (proc.status !== 0) {
80+
throw new Error(`${command} ${args.join(" ")} exited with status ${proc.status}`);
81+
}
82+
return proc;
83+
}
84+
85+
function pythonCandidates() {
86+
const candidates = [];
87+
if (process.env.WORD_AI_PYTHON) {
88+
candidates.push(process.env.WORD_AI_PYTHON);
89+
}
90+
candidates.push("python3", "python");
91+
return [...new Set(candidates)];
92+
}
93+
94+
function pythonVersion(command) {
95+
const proc = childProcess.spawnSync(
96+
command,
97+
["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')"],
98+
{encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], shell: false}
99+
);
100+
if (proc.error || proc.status !== 0) {
101+
return null;
102+
}
103+
const text = proc.stdout.trim();
104+
const parts = text.split(".").map((part) => Number.parseInt(part, 10));
105+
if (parts.length < 2 || Number.isNaN(parts[0]) || Number.isNaN(parts[1])) {
106+
return null;
107+
}
108+
return {text, major: parts[0], minor: parts[1]};
109+
}
110+
111+
function findPython() {
112+
for (const candidate of pythonCandidates()) {
113+
const version = pythonVersion(candidate);
114+
if (version && (version.major > 3 || (version.major === 3 && version.minor >= 10))) {
115+
return candidate;
116+
}
117+
}
118+
throw new Error("Word AI requires Python 3.10 or newer. Set WORD_AI_PYTHON=/path/to/python if needed.");
119+
}
120+
121+
function ensureVenv(root, version) {
122+
const venvDir = path.join(cacheRoot(), version, "venv");
123+
const lockDir = path.join(cacheRoot(), version, ".bootstrap.lock");
124+
const python = venvPython(venvDir);
125+
const marker = path.join(venvDir, ".word-ai-npm-version");
126+
const requirements = path.join(root, "requirements.txt");
127+
if (
128+
fs.existsSync(python) &&
129+
fs.existsSync(marker) &&
130+
fs.readFileSync(marker, "utf8").trim() === version
131+
) {
132+
return python;
133+
}
134+
135+
fs.mkdirSync(path.dirname(venvDir), {recursive: true});
136+
acquireLock(lockDir);
137+
try {
138+
if (
139+
fs.existsSync(python) &&
140+
fs.existsSync(marker) &&
141+
fs.readFileSync(marker, "utf8").trim() === version
142+
) {
143+
return python;
144+
}
145+
fs.rmSync(venvDir, {recursive: true, force: true});
146+
const systemPython = findPython();
147+
console.error(`Bootstrapping Word AI Python environment with ${systemPython}...`);
148+
runChecked(systemPython, ["-m", "venv", venvDir]);
149+
runChecked(python, ["-m", "pip", "install", "--upgrade", "pip"]);
150+
runChecked(python, ["-m", "pip", "install", "-r", requirements]);
151+
fs.writeFileSync(marker, `${version}\n`, "utf8");
152+
} finally {
153+
releaseLock(lockDir);
154+
}
155+
return python;
156+
}
157+
158+
function main(moduleName) {
159+
const root = packageRoot();
160+
const pkg = readPackage(root);
161+
const python = ensureVenv(root, pkg.version);
162+
const env = {...process.env};
163+
env.PYTHONPATH = root + (env.PYTHONPATH ? path.delimiter + env.PYTHONPATH : "");
164+
const child = childProcess.spawn(python, ["-m", moduleName, ...process.argv.slice(2)], {
165+
stdio: "inherit",
166+
env
167+
});
168+
child.on("error", (error) => {
169+
console.error(error.message);
170+
process.exit(1);
171+
});
172+
child.on("exit", (code, signal) => {
173+
if (signal) {
174+
process.kill(process.pid, signal);
175+
return;
176+
}
177+
process.exit(code === null ? 1 : code);
178+
});
179+
}
180+
181+
module.exports = {main};

bin/word-ai-mcp.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
require("./word-ai-launcher.cjs").main("word_ai_mcp.server");

bin/word-ai.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env node
2+
"use strict";
3+
4+
require("./word-ai-launcher.cjs").main("word_ai_mcp.quickstart");

0 commit comments

Comments
 (0)