apple-numbers-mcp 1.1.18 → 1.1.19
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 +8 -3
- package/build/index.js +150 -43
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -303,7 +303,7 @@ Case-insensitive partial match across every cell in a `.numbers` file.
|
|
|
303
303
|
|
|
304
304
|
Export a table to CSV, TSV, or JSON.
|
|
305
305
|
|
|
306
|
-
**⚠️ Safety:** Writes a file to disk at `outputPath`. The path is written unconditionally — any existing file there is **overwritten** — so confirm the destination before calling.
|
|
306
|
+
**⚠️ Safety:** Writes a file to disk at `outputPath`. The path is written unconditionally — any existing file there is **overwritten** — so confirm the destination before calling. Paths are bounded: `outputPath` must resolve — after `~` expansion and symlink resolution — to a location under your **home directory**, `/tmp`, `/private/tmp`, or `/Volumes`; anything else is rejected with an error naming those roots.
|
|
307
307
|
|
|
308
308
|
| Parameter | Type | Required | Description |
|
|
309
309
|
|-----------|------|----------|-------------|
|
|
@@ -321,7 +321,7 @@ Export a table to CSV, TSV, or JSON.
|
|
|
321
321
|
|
|
322
322
|
Create a new `.numbers` file with one sheet and table.
|
|
323
323
|
|
|
324
|
-
**⚠️ Safety:** Overwrites the file at `path` if it already exists — confirm the destination first.
|
|
324
|
+
**⚠️ Safety:** Overwrites the file at `path` if it already exists — confirm the destination first. Paths are bounded: `path` must resolve — after `~` expansion and symlink resolution — to a location under your **home directory**, `/tmp`, `/private/tmp`, or `/Volumes`; anything else is rejected with an error naming those roots.
|
|
325
325
|
|
|
326
326
|
| Parameter | Type | Required | Description |
|
|
327
327
|
|-----------|------|----------|-------------|
|
|
@@ -573,7 +573,7 @@ Merge a rectangular range of cells, or undo a merge.
|
|
|
573
573
|
|
|
574
574
|
Import a CSV, TSV, or JSON file into a new `.numbers` spreadsheet. Auto-detects format from extension, or pass `format` explicitly.
|
|
575
575
|
|
|
576
|
-
**⚠️ Safety:** Overwrites the file at `outputPath` if it already exists — confirm the destination first.
|
|
576
|
+
**⚠️ Safety:** Overwrites the file at `outputPath` if it already exists — confirm the destination first. Paths are bounded: both `inputPath` and `outputPath` must resolve — after `~` expansion and symlink resolution — to locations under your **home directory**, `/tmp`, `/private/tmp`, or `/Volumes`; anything else is rejected with an error naming those roots.
|
|
577
577
|
|
|
578
578
|
| Parameter | Type | Required | Description |
|
|
579
579
|
|-----------|------|----------|-------------|
|
|
@@ -774,6 +774,11 @@ These are tracked for future releases. The underlying `numbers-parser` library h
|
|
|
774
774
|
- Check the path; expand `~` if your shell isn't doing it.
|
|
775
775
|
- Ensure the file extension is `.numbers`.
|
|
776
776
|
|
|
777
|
+
### "Output path … is outside the allowed roots" / "Input path … is outside the allowed roots"
|
|
778
|
+
- `export-table`, `create-spreadsheet` and `import-csv` read and write ordinary files, so their paths are restricted to your **home directory**, `/tmp`, `/private/tmp`, and `/Volumes` — the server will not write into `/etc`, `/Library`, an app bundle, or any other system location, and will not read a source file from one.
|
|
779
|
+
- Symlinks are resolved **before** the check, so a link inside an allowed directory that points outside it is rejected too; the error reports the resolved path, which is usually the quickest way to see where a path really went.
|
|
780
|
+
- Fix: choose a path under one of those roots (`~/Documents/report.csv`, `/tmp/report.csv`). There is no override setting.
|
|
781
|
+
|
|
777
782
|
### Formatting / formula tools fail with "Numbers.app not running" or "Not authorized to send Apple events to Numbers"
|
|
778
783
|
- Open Numbers.app at least once. macOS will prompt for automation permission — accept it.
|
|
779
784
|
- Verify in **System Settings → Privacy & Security → Automation**, or reset with `tccutil reset AppleEvents`.
|
package/build/index.js
CHANGED
|
@@ -2985,7 +2985,7 @@ var require_compile = __commonJS({
|
|
|
2985
2985
|
const schOrFunc = root.refs[ref];
|
|
2986
2986
|
if (schOrFunc)
|
|
2987
2987
|
return schOrFunc;
|
|
2988
|
-
let _sch =
|
|
2988
|
+
let _sch = resolve3.call(this, root, ref);
|
|
2989
2989
|
if (_sch === void 0) {
|
|
2990
2990
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
2991
2991
|
const { schemaId } = this.opts;
|
|
@@ -3012,7 +3012,7 @@ var require_compile = __commonJS({
|
|
|
3012
3012
|
function sameSchemaEnv(s1, s2) {
|
|
3013
3013
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3014
3014
|
}
|
|
3015
|
-
function
|
|
3015
|
+
function resolve3(root, ref) {
|
|
3016
3016
|
let sch;
|
|
3017
3017
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3018
3018
|
ref = sch;
|
|
@@ -3643,7 +3643,7 @@ var require_fast_uri = __commonJS({
|
|
|
3643
3643
|
}
|
|
3644
3644
|
return uri;
|
|
3645
3645
|
}
|
|
3646
|
-
function
|
|
3646
|
+
function resolve3(baseURI, relativeURI, options) {
|
|
3647
3647
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
3648
3648
|
const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
|
|
3649
3649
|
const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
|
|
@@ -3927,7 +3927,7 @@ var require_fast_uri = __commonJS({
|
|
|
3927
3927
|
var fastUri = {
|
|
3928
3928
|
SCHEMES,
|
|
3929
3929
|
normalize,
|
|
3930
|
-
resolve:
|
|
3930
|
+
resolve: resolve3,
|
|
3931
3931
|
resolveComponent,
|
|
3932
3932
|
equal,
|
|
3933
3933
|
serialize,
|
|
@@ -19023,7 +19023,7 @@ var Protocol = class {
|
|
|
19023
19023
|
return;
|
|
19024
19024
|
}
|
|
19025
19025
|
const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
|
|
19026
|
-
await new Promise((
|
|
19026
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
19027
19027
|
options?.signal?.throwIfAborted();
|
|
19028
19028
|
}
|
|
19029
19029
|
} catch (error2) {
|
|
@@ -19040,7 +19040,7 @@ var Protocol = class {
|
|
|
19040
19040
|
*/
|
|
19041
19041
|
request(request, resultSchema, options) {
|
|
19042
19042
|
const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
|
|
19043
|
-
return new Promise((
|
|
19043
|
+
return new Promise((resolve3, reject) => {
|
|
19044
19044
|
const earlyReject = (error2) => {
|
|
19045
19045
|
reject(error2);
|
|
19046
19046
|
};
|
|
@@ -19118,7 +19118,7 @@ var Protocol = class {
|
|
|
19118
19118
|
if (!parseResult.success) {
|
|
19119
19119
|
reject(parseResult.error);
|
|
19120
19120
|
} else {
|
|
19121
|
-
|
|
19121
|
+
resolve3(parseResult.data);
|
|
19122
19122
|
}
|
|
19123
19123
|
} catch (error2) {
|
|
19124
19124
|
reject(error2);
|
|
@@ -19379,12 +19379,12 @@ var Protocol = class {
|
|
|
19379
19379
|
}
|
|
19380
19380
|
} catch {
|
|
19381
19381
|
}
|
|
19382
|
-
return new Promise((
|
|
19382
|
+
return new Promise((resolve3, reject) => {
|
|
19383
19383
|
if (signal.aborted) {
|
|
19384
19384
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
19385
19385
|
return;
|
|
19386
19386
|
}
|
|
19387
|
-
const timeoutId = setTimeout(
|
|
19387
|
+
const timeoutId = setTimeout(resolve3, interval);
|
|
19388
19388
|
signal.addEventListener("abort", () => {
|
|
19389
19389
|
clearTimeout(timeoutId);
|
|
19390
19390
|
reject(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
|
|
@@ -20697,7 +20697,7 @@ var McpServer = class {
|
|
|
20697
20697
|
let task = createTaskResult.task;
|
|
20698
20698
|
const pollInterval = task.pollInterval ?? 5e3;
|
|
20699
20699
|
while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
|
|
20700
|
-
await new Promise((
|
|
20700
|
+
await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
|
|
20701
20701
|
const updatedTask = await extra.taskStore.getTask(taskId);
|
|
20702
20702
|
if (!updatedTask) {
|
|
20703
20703
|
throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
|
|
@@ -21385,12 +21385,12 @@ var StdioServerTransport = class {
|
|
|
21385
21385
|
this.onclose?.();
|
|
21386
21386
|
}
|
|
21387
21387
|
send(message) {
|
|
21388
|
-
return new Promise((
|
|
21388
|
+
return new Promise((resolve3) => {
|
|
21389
21389
|
const json2 = serializeMessage(message);
|
|
21390
21390
|
if (this._stdout.write(json2)) {
|
|
21391
|
-
|
|
21391
|
+
resolve3();
|
|
21392
21392
|
} else {
|
|
21393
|
-
this._stdout.once("drain",
|
|
21393
|
+
this._stdout.once("drain", resolve3);
|
|
21394
21394
|
}
|
|
21395
21395
|
});
|
|
21396
21396
|
}
|
|
@@ -21399,7 +21399,7 @@ var StdioServerTransport = class {
|
|
|
21399
21399
|
// src/index.ts
|
|
21400
21400
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
21401
21401
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
21402
|
-
import { dirname as
|
|
21402
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
21403
21403
|
|
|
21404
21404
|
// src/utils/python.ts
|
|
21405
21405
|
import { execSync, execFileSync } from "node:child_process";
|
|
@@ -21812,14 +21812,110 @@ function unmergeCells(filePath, sheet, table, startRow, startCol, endRow, endCol
|
|
|
21812
21812
|
}
|
|
21813
21813
|
|
|
21814
21814
|
// src/services/numbersManager.ts
|
|
21815
|
-
import { existsSync as
|
|
21816
|
-
import { resolve, extname } from "node:path";
|
|
21817
|
-
import { homedir } from "node:os";
|
|
21815
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
21816
|
+
import { resolve as resolve2, extname } from "node:path";
|
|
21817
|
+
import { homedir as homedir2 } from "node:os";
|
|
21818
|
+
|
|
21819
|
+
// src/utils/exportPath.ts
|
|
21820
|
+
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
21821
|
+
import { homedir, tmpdir } from "node:os";
|
|
21822
|
+
import { basename, dirname as dirname2, isAbsolute, join as join2, resolve, sep } from "node:path";
|
|
21823
|
+
var ALLOWED_EXPORT_ROOTS = [
|
|
21824
|
+
homedir(),
|
|
21825
|
+
// os.tmpdir() is the canonical per-user temp dir on macOS (/var/folders/<hash>/T,
|
|
21826
|
+
// real path /private/var/folders/...). It is what Node's os.tmpdir(), Python's
|
|
21827
|
+
// tempfile and $TMPDIR all return — and what this repo's own fixtures use — so
|
|
21828
|
+
// omitting it refuses the most ordinary scratch destination there is.
|
|
21829
|
+
tmpdir(),
|
|
21830
|
+
"/private/var/folders",
|
|
21831
|
+
"/tmp",
|
|
21832
|
+
"/private/tmp",
|
|
21833
|
+
"/Volumes"
|
|
21834
|
+
];
|
|
21835
|
+
var ALLOWED_EXPORT_ROOTS_TEXT = "your home directory, /tmp, /private/tmp, or /Volumes";
|
|
21836
|
+
function canonicalize(path) {
|
|
21837
|
+
return realpathSync.native(path);
|
|
21838
|
+
}
|
|
21839
|
+
function allowedRoots() {
|
|
21840
|
+
const roots = [];
|
|
21841
|
+
for (const root of ALLOWED_EXPORT_ROOTS) {
|
|
21842
|
+
if (!roots.includes(root)) roots.push(root);
|
|
21843
|
+
try {
|
|
21844
|
+
const canonical = canonicalize(root);
|
|
21845
|
+
if (!roots.includes(canonical)) roots.push(canonical);
|
|
21846
|
+
} catch {
|
|
21847
|
+
}
|
|
21848
|
+
}
|
|
21849
|
+
return roots;
|
|
21850
|
+
}
|
|
21851
|
+
function isPathWithinAllowedRoots(resolvedPath) {
|
|
21852
|
+
return allowedRoots().some((root) => {
|
|
21853
|
+
const base = root.endsWith(sep) ? root.slice(0, -1) : root;
|
|
21854
|
+
return resolvedPath === base || resolvedPath.startsWith(base + sep);
|
|
21855
|
+
});
|
|
21856
|
+
}
|
|
21857
|
+
function expandTilde(p) {
|
|
21858
|
+
if (p === "~") return homedir();
|
|
21859
|
+
if (p.startsWith(`~${sep}`) || p.startsWith("~/")) return join2(homedir(), p.slice(2));
|
|
21860
|
+
return p;
|
|
21861
|
+
}
|
|
21862
|
+
function entryPresent(p) {
|
|
21863
|
+
try {
|
|
21864
|
+
lstatSync(p);
|
|
21865
|
+
return true;
|
|
21866
|
+
} catch (e) {
|
|
21867
|
+
const code = e.code;
|
|
21868
|
+
return !(code === "ENOENT" || code === "ENOTDIR");
|
|
21869
|
+
}
|
|
21870
|
+
}
|
|
21871
|
+
function canonicalizeCandidate(p, depth = 0) {
|
|
21872
|
+
if (depth > 32) throw new Error(`Too many symbolic links resolving "${p}"`);
|
|
21873
|
+
let existing = p;
|
|
21874
|
+
const tail = [];
|
|
21875
|
+
while (!entryPresent(existing)) {
|
|
21876
|
+
const parent = dirname2(existing);
|
|
21877
|
+
if (parent === existing) break;
|
|
21878
|
+
tail.unshift(basename(existing));
|
|
21879
|
+
existing = parent;
|
|
21880
|
+
}
|
|
21881
|
+
let real;
|
|
21882
|
+
try {
|
|
21883
|
+
real = canonicalize(existing);
|
|
21884
|
+
} catch {
|
|
21885
|
+
const viaLink = resolveDanglingLink(existing, depth);
|
|
21886
|
+
if (viaLink !== null) {
|
|
21887
|
+
return canonicalizeCandidate(tail.length ? join2(viaLink, ...tail) : viaLink, depth + 1);
|
|
21888
|
+
}
|
|
21889
|
+
real = existing;
|
|
21890
|
+
}
|
|
21891
|
+
return tail.length ? join2(real, ...tail) : real;
|
|
21892
|
+
}
|
|
21893
|
+
function resolveDanglingLink(p, depth) {
|
|
21894
|
+
try {
|
|
21895
|
+
if (!lstatSync(p).isSymbolicLink()) return null;
|
|
21896
|
+
const target = readlinkSync(p);
|
|
21897
|
+
if (isAbsolute(target)) return target;
|
|
21898
|
+
return resolve(canonicalizeCandidate(dirname2(p), depth + 1), target);
|
|
21899
|
+
} catch {
|
|
21900
|
+
return null;
|
|
21901
|
+
}
|
|
21902
|
+
}
|
|
21903
|
+
function resolveWithinAllowedRoots(path, label) {
|
|
21904
|
+
const resolved = canonicalizeCandidate(resolve(expandTilde(path)));
|
|
21905
|
+
if (!isPathWithinAllowedRoots(resolved)) {
|
|
21906
|
+
throw new Error(
|
|
21907
|
+
`${label} "${path}" resolves to "${resolved}", which is outside the allowed roots (${ALLOWED_EXPORT_ROOTS_TEXT}). Choose a path under one of those roots.`
|
|
21908
|
+
);
|
|
21909
|
+
}
|
|
21910
|
+
return resolved;
|
|
21911
|
+
}
|
|
21912
|
+
|
|
21913
|
+
// src/services/numbersManager.ts
|
|
21818
21914
|
var NumbersManager = class {
|
|
21819
21915
|
validatePath(filePath) {
|
|
21820
|
-
const expanded = filePath.startsWith("~") ? filePath.replace(/^~/,
|
|
21821
|
-
const resolved =
|
|
21822
|
-
if (!
|
|
21916
|
+
const expanded = filePath.startsWith("~") ? filePath.replace(/^~/, homedir2()) : filePath;
|
|
21917
|
+
const resolved = resolve2(expanded);
|
|
21918
|
+
if (!existsSync3(resolved)) {
|
|
21823
21919
|
throw new Error(`File not found: ${resolved}`);
|
|
21824
21920
|
}
|
|
21825
21921
|
if (extname(resolved).toLowerCase() !== ".numbers") {
|
|
@@ -21829,22 +21925,33 @@ var NumbersManager = class {
|
|
|
21829
21925
|
}
|
|
21830
21926
|
/**
|
|
21831
21927
|
* Resolve and validate an output path for new files.
|
|
21832
|
-
* Expands
|
|
21928
|
+
* Expands ~, enforces the allowed-roots boundary, and checks the .numbers
|
|
21929
|
+
* extension, but does NOT require the file to exist. The extension is checked
|
|
21930
|
+
* on the CANONICAL path so a symlink can't present a .numbers name for a
|
|
21931
|
+
* target that is something else entirely.
|
|
21833
21932
|
*/
|
|
21834
21933
|
validateOutputPath(filePath) {
|
|
21835
|
-
const
|
|
21836
|
-
const resolved = resolve(expanded);
|
|
21934
|
+
const resolved = resolveWithinAllowedRoots(filePath, "Output path");
|
|
21837
21935
|
if (extname(resolved).toLowerCase() !== ".numbers") {
|
|
21838
21936
|
throw new Error(`Not a Numbers file path: ${resolved}. Expected .numbers extension.`);
|
|
21839
21937
|
}
|
|
21840
21938
|
return resolved;
|
|
21841
21939
|
}
|
|
21842
21940
|
/**
|
|
21843
|
-
* Resolve a
|
|
21941
|
+
* Resolve a path that will be WRITTEN (export destinations), enforcing the
|
|
21942
|
+
* allowed-roots boundary. Symlinks are resolved before the check, so a link
|
|
21943
|
+
* inside an allowed directory cannot escape to a system location.
|
|
21844
21944
|
*/
|
|
21845
|
-
|
|
21846
|
-
|
|
21847
|
-
|
|
21945
|
+
resolveWritePath(filePath) {
|
|
21946
|
+
return resolveWithinAllowedRoots(filePath, "Output path");
|
|
21947
|
+
}
|
|
21948
|
+
/**
|
|
21949
|
+
* Resolve a path that will be READ (import sources), enforcing the same
|
|
21950
|
+
* allowed-roots boundary so this server can't be turned into an arbitrary
|
|
21951
|
+
* local-file reader.
|
|
21952
|
+
*/
|
|
21953
|
+
resolveReadPath(filePath) {
|
|
21954
|
+
return resolveWithinAllowedRoots(filePath, "Input path");
|
|
21848
21955
|
}
|
|
21849
21956
|
/**
|
|
21850
21957
|
* Get file structure: sheets, tables, dimensions, headers.
|
|
@@ -21886,7 +21993,7 @@ var NumbersManager = class {
|
|
|
21886
21993
|
*/
|
|
21887
21994
|
exportTable(filePath, format, outputPath, sheet, table) {
|
|
21888
21995
|
const resolved = this.validatePath(filePath);
|
|
21889
|
-
const outputResolved = this.
|
|
21996
|
+
const outputResolved = this.resolveWritePath(outputPath);
|
|
21890
21997
|
const args = [resolved, format, outputResolved];
|
|
21891
21998
|
if (sheet) args.push("--sheet", sheet);
|
|
21892
21999
|
if (table) args.push("--table", table);
|
|
@@ -22000,8 +22107,8 @@ var NumbersManager = class {
|
|
|
22000
22107
|
* Import a CSV/TSV/JSON file into a new .numbers file.
|
|
22001
22108
|
*/
|
|
22002
22109
|
importFile(inputPath, outputPath, options) {
|
|
22003
|
-
const inputResolved = this.
|
|
22004
|
-
if (!
|
|
22110
|
+
const inputResolved = this.resolveReadPath(inputPath);
|
|
22111
|
+
if (!existsSync3(inputResolved)) {
|
|
22005
22112
|
throw new Error(`Input file not found: ${inputResolved}`);
|
|
22006
22113
|
}
|
|
22007
22114
|
const outputResolved = this.validateOutputPath(outputPath);
|
|
@@ -22139,7 +22246,7 @@ function withErrorHandling(handler, prefix) {
|
|
|
22139
22246
|
|
|
22140
22247
|
// src/tools/doctor.ts
|
|
22141
22248
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
22142
|
-
import { existsSync as
|
|
22249
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
22143
22250
|
var NUMBERS_APP_PATHS = [
|
|
22144
22251
|
"/Applications/Numbers.app",
|
|
22145
22252
|
"/System/Applications/Numbers.app",
|
|
@@ -22148,7 +22255,7 @@ var NUMBERS_APP_PATHS = [
|
|
|
22148
22255
|
];
|
|
22149
22256
|
var NUMBERS_BUNDLE_IDS = ["com.apple.Numbers", "com.apple.iWork.Numbers"];
|
|
22150
22257
|
function findNumbersApp() {
|
|
22151
|
-
const byPath = NUMBERS_APP_PATHS.find((p) =>
|
|
22258
|
+
const byPath = NUMBERS_APP_PATHS.find((p) => existsSync4(p));
|
|
22152
22259
|
if (byPath) return byPath;
|
|
22153
22260
|
for (const id of NUMBERS_BUNDLE_IDS) {
|
|
22154
22261
|
try {
|
|
@@ -22158,7 +22265,7 @@ function findNumbersApp() {
|
|
|
22158
22265
|
{ encoding: "utf8", timeout: 5e3, stdio: ["ignore", "pipe", "ignore"] }
|
|
22159
22266
|
).trim();
|
|
22160
22267
|
const path = out.endsWith("/") ? out.slice(0, -1) : out;
|
|
22161
|
-
if (path &&
|
|
22268
|
+
if (path && existsSync4(path)) return path;
|
|
22162
22269
|
} catch {
|
|
22163
22270
|
}
|
|
22164
22271
|
}
|
|
@@ -22335,18 +22442,18 @@ First use get-file-info and read-table to read the relevant table(s) so you know
|
|
|
22335
22442
|
}
|
|
22336
22443
|
|
|
22337
22444
|
// src/services/fileConfig.ts
|
|
22338
|
-
import { existsSync as
|
|
22339
|
-
import { join as
|
|
22340
|
-
import { homedir as
|
|
22445
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2 } from "node:fs";
|
|
22446
|
+
import { join as join3 } from "node:path";
|
|
22447
|
+
import { homedir as homedir3 } from "node:os";
|
|
22341
22448
|
function fileConfigPath(env = process.env) {
|
|
22342
22449
|
const override = env.APPLE_NUMBERS_MCP_CONFIG_FILE;
|
|
22343
22450
|
if (override && override.trim()) return override.trim();
|
|
22344
|
-
return
|
|
22451
|
+
return join3(homedir3(), "Library", "Application Support", "apple-numbers-mcp", "config.json");
|
|
22345
22452
|
}
|
|
22346
22453
|
function loadFileConfig(env = process.env, path = fileConfigPath(env)) {
|
|
22347
22454
|
const applied = [];
|
|
22348
22455
|
try {
|
|
22349
|
-
if (!
|
|
22456
|
+
if (!existsSync5(path)) return applied;
|
|
22350
22457
|
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
22351
22458
|
if (!parsed || typeof parsed !== "object") return applied;
|
|
22352
22459
|
for (const [k, v] of Object.entries(parsed)) {
|
|
@@ -22476,8 +22583,8 @@ function withJsonSchema2020_12(transport) {
|
|
|
22476
22583
|
// src/index.ts
|
|
22477
22584
|
loadFileConfig();
|
|
22478
22585
|
var __filename2 = fileURLToPath2(import.meta.url);
|
|
22479
|
-
var __dirname2 =
|
|
22480
|
-
var pkg = JSON.parse(readFileSync3(
|
|
22586
|
+
var __dirname2 = dirname3(__filename2);
|
|
22587
|
+
var pkg = JSON.parse(readFileSync3(join4(__dirname2, "..", "package.json"), "utf-8"));
|
|
22481
22588
|
var version2 = pkg.version;
|
|
22482
22589
|
var manager = new NumbersManager();
|
|
22483
22590
|
var MAX_INDEX = 1e6;
|
|
@@ -22661,7 +22768,7 @@ ${lines.join("\n")}`, {
|
|
|
22661
22768
|
registerTool(
|
|
22662
22769
|
"export-table",
|
|
22663
22770
|
{
|
|
22664
|
-
description: "Use when: you want to write a table's data out to a CSV, TSV, or JSON file on disk. Defaults to the first sheet and first table if sheet/table are omitted.\nReturns: the exported row count, format, and output path.\nDo not use when: you want the data inline in the response rather than a file \u2014 use read-table; or you want to build a new .numbers file from a CSV/TSV/JSON source \u2014 use import-csv.\nSafety: writes a file to disk at outputPath. The path is written unconditionally \u2014 any existing file there is OVERWRITTEN \u2014 so confirm the destination before calling.",
|
|
22771
|
+
description: "Use when: you want to write a table's data out to a CSV, TSV, or JSON file on disk. Defaults to the first sheet and first table if sheet/table are omitted.\nReturns: the exported row count, format, and output path.\nDo not use when: you want the data inline in the response rather than a file \u2014 use read-table; or you want to build a new .numbers file from a CSV/TSV/JSON source \u2014 use import-csv.\nSafety: writes a file to disk at outputPath. The path is written unconditionally \u2014 any existing file there is OVERWRITTEN \u2014 so confirm the destination before calling. outputPath must resolve \u2014 after ~ expansion and symlink resolution \u2014 to a path under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected with an error naming those roots.",
|
|
22665
22772
|
inputSchema: {
|
|
22666
22773
|
path: external_exports.string().describe("Path to the .numbers file"),
|
|
22667
22774
|
format: external_exports.enum(["csv", "tsv", "json"]).describe("Output format"),
|
|
@@ -22725,7 +22832,7 @@ Merged: yes`;
|
|
|
22725
22832
|
registerTool(
|
|
22726
22833
|
"create-spreadsheet",
|
|
22727
22834
|
{
|
|
22728
|
-
description: "Use when: you want to create a brand-new .numbers file with a single sheet and table from a list of headers, optionally with initial data rows.\nReturns: the file path, sheet name, table name, header count, and data-row count.\nDo not use when: you want to build the file from an existing CSV/TSV/JSON source \u2014 use import-csv; or you want to add to a file that already exists \u2014 use add-sheet / add-table / add-rows.\nSafety: writes a .numbers file via the numbers-parser sidecar (does not require Numbers.app). The target path is written unconditionally \u2014 if a file already exists at that path it is OVERWRITTEN in place; choose a fresh path or confirm overwrite first.",
|
|
22835
|
+
description: "Use when: you want to create a brand-new .numbers file with a single sheet and table from a list of headers, optionally with initial data rows.\nReturns: the file path, sheet name, table name, header count, and data-row count.\nDo not use when: you want to build the file from an existing CSV/TSV/JSON source \u2014 use import-csv; or you want to add to a file that already exists \u2014 use add-sheet / add-table / add-rows.\nSafety: writes a .numbers file via the numbers-parser sidecar (does not require Numbers.app). The target path is written unconditionally \u2014 if a file already exists at that path it is OVERWRITTEN in place; choose a fresh path or confirm overwrite first. path must resolve \u2014 after ~ expansion and symlink resolution \u2014 to a location under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected with an error naming those roots.",
|
|
22729
22836
|
inputSchema: {
|
|
22730
22837
|
path: external_exports.string().describe("Path for the new .numbers file"),
|
|
22731
22838
|
headers: external_exports.array(headerCellSchema).min(1).max(MAX_BATCH).describe("Column header names"),
|
|
@@ -22931,7 +23038,7 @@ registerTool(
|
|
|
22931
23038
|
registerTool(
|
|
22932
23039
|
"import-csv",
|
|
22933
23040
|
{
|
|
22934
|
-
description: 'Use when: you want to convert an existing CSV, TSV, or JSON file into a new .numbers spreadsheet. Format is auto-detected from the input file extension unless you specify it explicitly; any unrecognized extension falls back to CSV. CSV/TSV fields are auto-typed (numbers, booleans, empty cells), so zero-padded identifiers LOSE their leading zeros \u2014 "01234" becomes 1234. JSON values are passed through untouched, but an array of objects takes its column set from the FIRST object only, silently dropping keys that appear later.\nReturns: the imported row and column counts, detected format, and the input/output paths plus sheet/table names.\nDo not use when: building a file from headers/rows you already have in hand \u2014 use create-spreadsheet; or exporting a .numbers table out to CSV/TSV/JSON \u2014 use export-table.\nSafety: writes the output .numbers file via the numbers-parser sidecar (does not require Numbers.app). The output path is written unconditionally \u2014 if a file already exists there it is OVERWRITTEN in place; choose a fresh output path or confirm overwrite first.',
|
|
23041
|
+
description: 'Use when: you want to convert an existing CSV, TSV, or JSON file into a new .numbers spreadsheet. Format is auto-detected from the input file extension unless you specify it explicitly; any unrecognized extension falls back to CSV. CSV/TSV fields are auto-typed (numbers, booleans, empty cells), so zero-padded identifiers LOSE their leading zeros \u2014 "01234" becomes 1234. JSON values are passed through untouched, but an array of objects takes its column set from the FIRST object only, silently dropping keys that appear later.\nReturns: the imported row and column counts, detected format, and the input/output paths plus sheet/table names.\nDo not use when: building a file from headers/rows you already have in hand \u2014 use create-spreadsheet; or exporting a .numbers table out to CSV/TSV/JSON \u2014 use export-table.\nSafety: writes the output .numbers file via the numbers-parser sidecar (does not require Numbers.app). The output path is written unconditionally \u2014 if a file already exists there it is OVERWRITTEN in place; choose a fresh output path or confirm overwrite first. Both inputPath and outputPath must resolve \u2014 after ~ expansion and symlink resolution \u2014 to locations under your home directory, /tmp, /private/tmp, or /Volumes; anything else is rejected with an error naming those roots.',
|
|
22935
23042
|
inputSchema: {
|
|
22936
23043
|
inputPath: external_exports.string().describe("Path to the CSV/TSV/JSON input file"),
|
|
22937
23044
|
outputPath: external_exports.string().describe("Path for the output .numbers file"),
|
package/package.json
CHANGED