aioffice 1.6.0

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 ADDED
@@ -0,0 +1,112 @@
1
+ # aioffice
2
+
3
+ **AIOffice** is an AI-native command-line tool and [MCP](https://modelcontextprotocol.io)
4
+ server for working with real Office documents — `.docx`, `.xlsx`, and `.pptx`.
5
+ It creates, reads, queries, edits, renders, validates, and converts Office files
6
+ through a single stable JSON surface designed for AI agents and scripts.
7
+
8
+ This npm package is a thin installer: on install it downloads the correct
9
+ self-contained native binary for your platform from the official
10
+ [GitHub release](https://github.com/onecer/AIOffice/releases) and verifies it
11
+ against the release's `SHA256SUMS`. There are no other dependencies.
12
+
13
+ ## Install
14
+
15
+ Global install (adds an `aioffice` command to your PATH):
16
+
17
+ ```sh
18
+ npm install -g aioffice
19
+ aioffice version
20
+ aioffice doctor
21
+ ```
22
+
23
+ Or run on demand without installing, via `npx`:
24
+
25
+ ```sh
26
+ npx aioffice doctor
27
+ npx aioffice create report.docx --title "Q3 Report"
28
+ ```
29
+
30
+ > On the first run, `npx` will download and SHA256-verify the native binary if
31
+ > the postinstall step did not already do so.
32
+
33
+ ## Use as an MCP server
34
+
35
+ AIOffice speaks MCP over stdio. Point your MCP client at the `aioffice mcp`
36
+ command:
37
+
38
+ ```json
39
+ {
40
+ "mcpServers": {
41
+ "aioffice": {
42
+ "command": "aioffice",
43
+ "args": ["mcp"]
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ If you installed globally, `aioffice` resolves from your PATH. If you prefer not
50
+ to install globally, use `npx` as the command instead:
51
+
52
+ ```json
53
+ {
54
+ "mcpServers": {
55
+ "aioffice": {
56
+ "command": "npx",
57
+ "args": ["-y", "aioffice", "mcp"]
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ The MCP transport is plain stdio JSON-RPC; the npm shim passes stdin/stdout
64
+ through transparently.
65
+
66
+ ## How installation works
67
+
68
+ 1. `install.js` (run as a postinstall script) detects your platform and CPU
69
+ architecture and maps them to the matching release asset:
70
+
71
+ | OS / arch | Asset |
72
+ | ---------------- | -------------------------- |
73
+ | macOS arm64 | `aioffice-mac-arm64` |
74
+ | macOS x64 | `aioffice-mac-x64` |
75
+ | Linux x64 | `aioffice-linux-x64` |
76
+ | Linux arm64 | `aioffice-linux-arm64` |
77
+ | Windows x64 | `aioffice-win-x64.exe` |
78
+ | Windows arm64 | `aioffice-win-arm64.exe` |
79
+
80
+ 2. It downloads `SHA256SUMS` and the binary from the release
81
+ `v{package version}`, computes the SHA256 of the download, and **refuses to
82
+ install** if it does not match the published checksum.
83
+ 3. On success the binary is placed in this package's `bin/` directory and made
84
+ executable (`chmod +x` on Unix). The install is idempotent — re-running it
85
+ skips work when a verified binary is already present.
86
+
87
+ If the download or verification fails, installation aborts with a clear message
88
+ that includes the direct download URL so you can install the binary manually.
89
+
90
+ ## Environment overrides
91
+
92
+ Both are useful for testing or for serving binaries from a private mirror:
93
+
94
+ | Variable | Default | Purpose |
95
+ | ---------------------------- | ---------------------------------------------------------------- | ---------------------------------------- |
96
+ | `AIOFFICE_DOWNLOAD_VERSION` | `v{package version}` (e.g. `v1.6.0`) | Release tag to download. |
97
+ | `AIOFFICE_DOWNLOAD_BASEURL` | `https://github.com/onecer/AIOffice/releases/download` | Base URL for the assets + `SHA256SUMS`. |
98
+
99
+ The binary is fetched from `{BASEURL}/{VERSION}/{asset}` and the checksum file
100
+ from `{BASEURL}/{VERSION}/SHA256SUMS`.
101
+
102
+ ```sh
103
+ # Example: install a specific version from a local mirror
104
+ AIOFFICE_DOWNLOAD_VERSION=v1.6.0 \
105
+ AIOFFICE_DOWNLOAD_BASEURL=https://mirror.example.com/aioffice \
106
+ npm install -g aioffice
107
+ ```
108
+
109
+ ## License
110
+
111
+ Apache-2.0. See the [AIOffice repository](https://github.com/onecer/AIOffice)
112
+ for source, full documentation, and the AI-facing surface contract.
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // bin/aioffice.js — thin launcher for the AIOffice native binary.
5
+ //
6
+ // Locates the downloaded binary (bin/aioffice[.exe]). If it is missing (e.g.
7
+ // postinstall was skipped, as `npm` does with --ignore-scripts, or under some
8
+ // `npx` flows), it runs the install step on demand, then spawns the binary
9
+ // with the inherited argv and stdio (full pass-through). The child's exit code
10
+ // is propagated. stdio:'inherit' keeps `aioffice mcp` (stdin/stdout JSON-RPC)
11
+ // transparent.
12
+
13
+ const path = require('path');
14
+ const fs = require('fs');
15
+ const { spawn } = require('child_process');
16
+
17
+ const { binaryName } = require('../platform');
18
+
19
+ const BINARY = path.join(__dirname, binaryName());
20
+
21
+ function spawnBinary() {
22
+ if (!fs.existsSync(BINARY)) {
23
+ process.stderr.write(
24
+ 'aioffice: native binary not found after install. ' +
25
+ 'See messages above; you may need to reinstall or download it ' +
26
+ 'manually from https://github.com/onecer/AIOffice/releases\n',
27
+ );
28
+ process.exit(1);
29
+ }
30
+
31
+ const child = spawn(BINARY, process.argv.slice(2), { stdio: 'inherit' });
32
+
33
+ child.on('error', (err) => {
34
+ process.stderr.write(`aioffice: failed to launch binary: ${err.message}\n`);
35
+ process.exit(1);
36
+ });
37
+
38
+ // Forward termination signals so Ctrl-C / kill reach the native process.
39
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
40
+ process.on(sig, () => {
41
+ if (!child.killed) {
42
+ try {
43
+ child.kill(sig);
44
+ } catch (_) {
45
+ /* ignore */
46
+ }
47
+ }
48
+ });
49
+ }
50
+
51
+ child.on('exit', (code, signal) => {
52
+ if (signal) {
53
+ // Re-raise the signal so the parent's exit status reflects it.
54
+ process.kill(process.pid, signal);
55
+ return;
56
+ }
57
+ process.exit(code == null ? 0 : code);
58
+ });
59
+ }
60
+
61
+ async function main() {
62
+ if (!fs.existsSync(BINARY)) {
63
+ // Lazy install (postinstall was skipped or `npx` first run).
64
+ process.stderr.write('aioffice: binary not present, installing...\n');
65
+ try {
66
+ const install = require('../install');
67
+ await install.main();
68
+ } catch (err) {
69
+ process.stderr.write(
70
+ (err && err.message ? err.message : String(err)) + '\n',
71
+ );
72
+ process.exit(1);
73
+ }
74
+ }
75
+ spawnBinary();
76
+ }
77
+
78
+ main();
package/install.js ADDED
@@ -0,0 +1,239 @@
1
+ 'use strict';
2
+
3
+ // install.js — postinstall step for the "aioffice" npm package.
4
+ //
5
+ // Downloads the platform-specific AIOffice binary from the matching GitHub
6
+ // release, verifies it against the release's SHA256SUMS file, and installs it
7
+ // (executable) into bin/. Uses ONLY Node builtins (https, fs, path, crypto) —
8
+ // no third-party dependencies.
9
+ //
10
+ // Environment overrides (handy for testing + private mirrors):
11
+ // AIOFFICE_DOWNLOAD_VERSION — release tag to download (default: v{package version})
12
+ // AIOFFICE_DOWNLOAD_BASEURL — base URL for the release assets
13
+ // (default: https://github.com/onecer/AIOffice/releases/download)
14
+ //
15
+ // The full asset URL is: {BASEURL}/{VERSION}/{asset}
16
+ // and the checksum file: {BASEURL}/{VERSION}/SHA256SUMS
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const https = require('https');
21
+ const crypto = require('crypto');
22
+
23
+ const { assetName, binaryName } = require('./platform');
24
+ const pkg = require('./package.json');
25
+
26
+ const DEFAULT_BASEURL = 'https://github.com/onecer/AIOffice/releases/download';
27
+ const REPO_RELEASES = 'https://github.com/onecer/AIOffice/releases';
28
+
29
+ const BIN_DIR = path.join(__dirname, 'bin');
30
+
31
+ // Resolve the release tag. A bare package version "1.6.0" becomes "v1.6.0";
32
+ // an explicit override is used verbatim (it may or may not carry a leading v).
33
+ function resolveVersion() {
34
+ const override = process.env.AIOFFICE_DOWNLOAD_VERSION;
35
+ if (override && override.trim()) return override.trim();
36
+ return `v${pkg.version}`;
37
+ }
38
+
39
+ function resolveBaseUrl() {
40
+ const override = process.env.AIOFFICE_DOWNLOAD_BASEURL;
41
+ if (override && override.trim()) return override.trim().replace(/\/+$/, '');
42
+ return DEFAULT_BASEURL;
43
+ }
44
+
45
+ // GET a URL into a Buffer, following redirects (GitHub asset URLs 302 to a CDN).
46
+ function fetchBuffer(url, redirectsLeft = 5) {
47
+ return new Promise((resolve, reject) => {
48
+ const req = https.get(
49
+ url,
50
+ { headers: { 'User-Agent': `aioffice-npm/${pkg.version}`, Accept: '*/*' } },
51
+ (res) => {
52
+ const { statusCode, headers } = res;
53
+ if (statusCode >= 300 && statusCode < 400 && headers.location) {
54
+ res.resume(); // drain
55
+ if (redirectsLeft <= 0) {
56
+ reject(new Error(`Too many redirects fetching ${url}`));
57
+ return;
58
+ }
59
+ const next = new URL(headers.location, url).toString();
60
+ resolve(fetchBuffer(next, redirectsLeft - 1));
61
+ return;
62
+ }
63
+ if (statusCode !== 200) {
64
+ res.resume();
65
+ reject(
66
+ new Error(
67
+ `Download failed (HTTP ${statusCode}) for ${url}`,
68
+ ),
69
+ );
70
+ return;
71
+ }
72
+ const chunks = [];
73
+ res.on('data', (c) => chunks.push(c));
74
+ res.on('end', () => resolve(Buffer.concat(chunks)));
75
+ },
76
+ );
77
+ req.on('error', reject);
78
+ req.setTimeout(120000, () => {
79
+ req.destroy(new Error(`Download timed out after 120s for ${url}`));
80
+ });
81
+ });
82
+ }
83
+
84
+ function sha256(buf) {
85
+ return crypto.createHash('sha256').update(buf).digest('hex');
86
+ }
87
+
88
+ // Parse a sha256sum-style file: lines of "<hex>␠␠<filename>".
89
+ // Returns a Map of filename -> lowercase hex digest.
90
+ function parseSha256Sums(text) {
91
+ const map = new Map();
92
+ for (const raw of text.split(/\r?\n/)) {
93
+ const line = raw.trim();
94
+ if (!line) continue;
95
+ const m = line.match(/^([0-9a-fA-F]{64})[\s*]+(.+)$/);
96
+ if (!m) continue;
97
+ map.set(m[2].trim(), m[1].toLowerCase());
98
+ }
99
+ return map;
100
+ }
101
+
102
+ async function main() {
103
+ const version = resolveVersion();
104
+ const baseUrl = resolveBaseUrl();
105
+ const asset = assetName(); // throws a clear error on unsupported platform
106
+ const localName = binaryName();
107
+ const dest = path.join(BIN_DIR, localName);
108
+
109
+ const assetUrl = `${baseUrl}/${version}/${asset}`;
110
+ const sumsUrl = `${baseUrl}/${version}/SHA256SUMS`;
111
+
112
+ fs.mkdirSync(BIN_DIR, { recursive: true });
113
+
114
+ // --- Idempotency: skip if a correct binary is already present. -----------
115
+ // We need the expected digest to know it's correct, so fetch SHA256SUMS
116
+ // first. If the network is unavailable but a binary already exists, trust it
117
+ // (a prior install verified it) rather than failing a re-run.
118
+ let sums;
119
+ try {
120
+ const sumsBuf = await fetchBuffer(sumsUrl);
121
+ sums = parseSha256Sums(sumsBuf.toString('utf8'));
122
+ } catch (err) {
123
+ if (fs.existsSync(dest) && fs.statSync(dest).size > 0) {
124
+ console.error(
125
+ `aioffice: could not fetch SHA256SUMS (${err.message}); ` +
126
+ 'a binary is already installed, keeping it.',
127
+ );
128
+ return;
129
+ }
130
+ fail(
131
+ `Could not download the checksum file.\n ${err.message}`,
132
+ version,
133
+ asset,
134
+ assetUrl,
135
+ );
136
+ }
137
+
138
+ const expected = sums.get(asset);
139
+ if (!expected) {
140
+ fail(
141
+ `Release ${version} has no checksum entry for "${asset}".\n` +
142
+ ` Known entries: ${[...sums.keys()].join(', ') || '(none)'}`,
143
+ version,
144
+ asset,
145
+ assetUrl,
146
+ );
147
+ }
148
+
149
+ if (fs.existsSync(dest) && fs.statSync(dest).size > 0) {
150
+ const have = sha256(fs.readFileSync(dest));
151
+ if (have === expected) {
152
+ ensureExecutable(dest);
153
+ console.log(
154
+ `aioffice: ${localName} already installed and verified (${version}).`,
155
+ );
156
+ return;
157
+ }
158
+ // Present but wrong (partial/stale download) — re-download below.
159
+ console.error('aioffice: existing binary failed verification, re-downloading.');
160
+ }
161
+
162
+ // --- Download the binary. -------------------------------------------------
163
+ console.log(`aioffice: downloading ${asset} (${version})...`);
164
+ let binBuf;
165
+ try {
166
+ binBuf = await fetchBuffer(assetUrl);
167
+ } catch (err) {
168
+ fail(
169
+ `Could not download the binary.\n ${err.message}`,
170
+ version,
171
+ asset,
172
+ assetUrl,
173
+ );
174
+ }
175
+
176
+ // --- Verify SHA256 BEFORE writing the final file. ------------------------
177
+ const actual = sha256(binBuf);
178
+ if (actual !== expected) {
179
+ fail(
180
+ 'SHA256 checksum mismatch — the download may be corrupt or tampered ' +
181
+ 'with. Nothing was installed.\n' +
182
+ ` expected: ${expected}\n actual: ${actual}`,
183
+ version,
184
+ asset,
185
+ assetUrl,
186
+ );
187
+ }
188
+
189
+ // Write atomically: temp file -> rename.
190
+ const tmp = `${dest}.download-${process.pid}`;
191
+ fs.writeFileSync(tmp, binBuf);
192
+ fs.renameSync(tmp, dest);
193
+ ensureExecutable(dest);
194
+
195
+ console.log(
196
+ `aioffice: installed ${localName} (${version}), SHA256 verified.`,
197
+ );
198
+ }
199
+
200
+ function ensureExecutable(file) {
201
+ if (process.platform !== 'win32') {
202
+ try {
203
+ fs.chmodSync(file, 0o755);
204
+ } catch (_) {
205
+ /* best-effort */
206
+ }
207
+ }
208
+ }
209
+
210
+ function fail(message, version, asset, assetUrl) {
211
+ const lines = [
212
+ '',
213
+ 'aioffice: installation failed.',
214
+ ` ${message.replace(/\n/g, '\n ')}`,
215
+ '',
216
+ 'You can download the binary manually from:',
217
+ ` ${assetUrl}`,
218
+ ` (release page: ${REPO_RELEASES}/tag/${version})`,
219
+ 'then place it in this package\'s bin/ directory as ' +
220
+ `"${binaryName()}"` +
221
+ (process.platform === 'win32' ? '.' : ' and run chmod +x on it.'),
222
+ '',
223
+ 'Env overrides: AIOFFICE_DOWNLOAD_VERSION, AIOFFICE_DOWNLOAD_BASEURL.',
224
+ '',
225
+ ];
226
+ const err = new Error(lines.join('\n'));
227
+ err.handled = true;
228
+ throw err;
229
+ }
230
+
231
+ // Allow `require('./install')` (used by the bin shim) to call main() directly.
232
+ module.exports = { main, parseSha256Sums, resolveVersion, resolveBaseUrl };
233
+
234
+ if (require.main === module) {
235
+ main().catch((err) => {
236
+ process.stderr.write((err && err.message ? err.message : String(err)) + '\n');
237
+ process.exit(1);
238
+ });
239
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "aioffice",
3
+ "version": "1.6.0",
4
+ "description": "AIOffice — an AI-native CLI + MCP server for creating, reading, editing, rendering and validating real Office documents (.docx/.xlsx/.pptx). Installs a single self-contained native binary downloaded and SHA256-verified from the GitHub release.",
5
+ "bin": {
6
+ "aioffice": "bin/aioffice.js"
7
+ },
8
+ "scripts": {
9
+ "postinstall": "node install.js"
10
+ },
11
+ "files": [
12
+ "bin/aioffice.js",
13
+ "install.js",
14
+ "platform.js"
15
+ ],
16
+ "engines": {
17
+ "node": ">=18"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/onecer/AIOffice.git"
22
+ },
23
+ "homepage": "https://github.com/onecer/AIOffice#readme",
24
+ "bugs": {
25
+ "url": "https://github.com/onecer/AIOffice/issues"
26
+ },
27
+ "keywords": [
28
+ "office",
29
+ "docx",
30
+ "xlsx",
31
+ "pptx",
32
+ "ooxml",
33
+ "mcp",
34
+ "ai",
35
+ "cli",
36
+ "word",
37
+ "excel",
38
+ "powerpoint",
39
+ "automation"
40
+ ],
41
+ "license": "Apache-2.0",
42
+ "author": "onecer"
43
+ }
package/platform.js ADDED
@@ -0,0 +1,67 @@
1
+ 'use strict';
2
+
3
+ // platform.js — maps the running platform to the GitHub release asset name and
4
+ // to the local binary filename. This is the single source of truth for the
5
+ // platform mapping; install.js and bin/aioffice.js both import it.
6
+ //
7
+ // Asset map (must match the names of the assets attached to each GitHub
8
+ // release v{version}):
9
+ // darwin + arm64 -> aioffice-mac-arm64
10
+ // darwin + x64 -> aioffice-mac-x64
11
+ // linux + x64 -> aioffice-linux-x64
12
+ // linux + arm64 -> aioffice-linux-arm64
13
+ // win32 + x64 -> aioffice-win-x64.exe
14
+ // win32 + arm64 -> aioffice-win-arm64.exe
15
+
16
+ // platform -> arch -> release asset name
17
+ const ASSETS = {
18
+ darwin: {
19
+ arm64: 'aioffice-mac-arm64',
20
+ x64: 'aioffice-mac-x64',
21
+ },
22
+ linux: {
23
+ x64: 'aioffice-linux-x64',
24
+ arm64: 'aioffice-linux-arm64',
25
+ },
26
+ win32: {
27
+ x64: 'aioffice-win-x64.exe',
28
+ arm64: 'aioffice-win-arm64.exe',
29
+ },
30
+ };
31
+
32
+ /**
33
+ * The local filename the downloaded binary is stored under, inside bin/.
34
+ * Windows needs a .exe extension so the OS will execute it; every other
35
+ * platform uses a plain "aioffice".
36
+ * @param {string} [platform=process.platform]
37
+ * @returns {string}
38
+ */
39
+ function binaryName(platform = process.platform) {
40
+ return platform === 'win32' ? 'aioffice.exe' : 'aioffice';
41
+ }
42
+
43
+ /**
44
+ * Resolve the GitHub release asset name for the given platform/arch.
45
+ * Throws a clear, actionable error on an unsupported combination.
46
+ * @param {string} [platform=process.platform]
47
+ * @param {string} [arch=process.arch]
48
+ * @returns {string} the release asset filename
49
+ */
50
+ function assetName(platform = process.platform, arch = process.arch) {
51
+ const byArch = ASSETS[platform];
52
+ const asset = byArch && byArch[arch];
53
+ if (!asset) {
54
+ const supported = Object.entries(ASSETS)
55
+ .flatMap(([p, m]) => Object.keys(m).map((a) => `${p}/${a}`))
56
+ .join(', ');
57
+ throw new Error(
58
+ `aioffice: unsupported platform "${platform}/${arch}". ` +
59
+ `Supported: ${supported}. ` +
60
+ 'If you believe this platform should be supported, please open an ' +
61
+ 'issue at https://github.com/onecer/AIOffice/issues',
62
+ );
63
+ }
64
+ return asset;
65
+ }
66
+
67
+ module.exports = { ASSETS, assetName, binaryName };