@stll/folio-cli 0.0.1-placeholder.0 → 0.1.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.
Files changed (45) hide show
  1. package/README.md +298 -0
  2. package/dist/bin.d.ts +1 -0
  3. package/dist/bin.js +30 -0
  4. package/dist/cli.d.ts +25 -0
  5. package/dist/cli.js +440 -0
  6. package/dist/document.d.ts +43 -0
  7. package/dist/document.js +127 -0
  8. package/dist/errors.d.ts +67 -0
  9. package/dist/errors.js +92 -0
  10. package/dist/execute-read.d.ts +64 -0
  11. package/dist/execute-read.js +220 -0
  12. package/dist/execute-write.d.ts +36 -0
  13. package/dist/execute-write.js +449 -0
  14. package/dist/flags.d.ts +38 -0
  15. package/dist/flags.js +102 -0
  16. package/dist/journal.d.ts +82 -0
  17. package/dist/journal.js +212 -0
  18. package/dist/lock.d.ts +55 -0
  19. package/dist/lock.js +157 -0
  20. package/dist/mcp.d.ts +28 -0
  21. package/dist/mcp.js +310 -0
  22. package/dist/output-file.d.ts +14 -0
  23. package/dist/output-file.js +60 -0
  24. package/dist/output.d.ts +33 -0
  25. package/dist/output.js +77 -0
  26. package/dist/package-parts.d.ts +24 -0
  27. package/dist/package-parts.js +172 -0
  28. package/dist/package.js +4 -0
  29. package/dist/preview-commands.d.ts +8 -0
  30. package/dist/preview-commands.js +226 -0
  31. package/dist/provenance.d.ts +17 -0
  32. package/dist/provenance.js +53 -0
  33. package/dist/registry.d.ts +69 -0
  34. package/dist/registry.js +255 -0
  35. package/dist/render.d.ts +35 -0
  36. package/dist/render.js +120 -0
  37. package/dist/roots.d.ts +21 -0
  38. package/dist/roots.js +57 -0
  39. package/dist/serve.d.ts +17 -0
  40. package/dist/serve.js +234 -0
  41. package/dist/sidecar.d.ts +52 -0
  42. package/dist/sidecar.js +156 -0
  43. package/dist/transaction.d.ts +55 -0
  44. package/dist/transaction.js +218 -0
  45. package/package.json +85 -3
@@ -0,0 +1,212 @@
1
+ import { errnoCode, fileVersionOf, sameFile } from "./document.js";
2
+ import { FOLIO_CLI_ERROR_CODES, cliError } from "./errors.js";
3
+ import { checkPackageIntegrity } from "./package-parts.js";
4
+ import { SIDECAR_DIRECTORY, ensureSidecarParent, inspectPath, openSidecarFile, readSidecarFile, syncDirectory } from "./sidecar.js";
5
+ import { Result } from "better-result";
6
+ import { constants } from "node:fs";
7
+ import { readdir, rename, rm } from "node:fs/promises";
8
+ import path from "node:path";
9
+ //#region src/journal.ts
10
+ /**
11
+ * The transaction journal: `.folio/journal.jsonl` beside the document (or
12
+ * `--journal`), one JSON line per committed transaction plus one per crash
13
+ * recovery. A `commit` line is written, and synced, after the staged package
14
+ * validates and before it is renamed into place, so the line is the commit
15
+ * point: a stage left behind by a crash is rolled forward when its line
16
+ * names this document, the stage is the validated package it recorded, and
17
+ * the destination is still what the transaction replaced. Anything else is
18
+ * discarded. The journal, like every sidecar file, is never read or written
19
+ * through a symlink or a hard link.
20
+ */
21
+ const RECOVERY_DISCARD_REASONS = [
22
+ "notJournaled",
23
+ "otherDocument",
24
+ "notRegularFile",
25
+ "stageChanged",
26
+ "invalidPackage",
27
+ "destinationMoved",
28
+ "leaseLost",
29
+ "renameRefused"
30
+ ];
31
+ const journalPathFor = (documentPath, override) => override === void 0 ? path.join(path.dirname(documentPath), SIDECAR_DIRECTORY, "journal.jsonl") : path.resolve(override);
32
+ const journalError = (message, error) => cliError({
33
+ code: FOLIO_CLI_ERROR_CODES.internal,
34
+ message: `${message}: ${error instanceof Error ? error.message : String(error)}`
35
+ });
36
+ /** Append one line and sync it to disk before returning. */
37
+ const appendJournal = async (journalPath, entry) => {
38
+ const parent = await ensureSidecarParent(journalPath);
39
+ if (parent.isErr()) return Result.err(parent.error);
40
+ const opened = await openSidecarFile(journalPath, constants.O_WRONLY | constants.O_APPEND | constants.O_CREAT);
41
+ if (opened.isErr()) return Result.err(opened.error);
42
+ const { handle } = opened.value;
43
+ const written = await Result.tryPromise(async () => {
44
+ try {
45
+ await handle.writeFile(`${JSON.stringify(entry)}\n`);
46
+ await handle.sync();
47
+ } finally {
48
+ await handle.close();
49
+ }
50
+ });
51
+ if (written.isErr()) return Result.err(journalError(`Cannot append to ${journalPath}`, written.error.cause));
52
+ return await syncDirectory(path.dirname(journalPath));
53
+ };
54
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
55
+ const parseCommit = (line) => {
56
+ const parsed = Result.try(() => JSON.parse(line));
57
+ if (parsed.isErr() || !isRecord(parsed.value)) return null;
58
+ const { type, txId, path: committedPath, requestHash, toVersion, destinationVersionBefore, receipt } = parsed.value;
59
+ return type === "commit" && typeof txId === "string" && typeof committedPath === "string" && typeof requestHash === "string" && typeof toVersion === "string" && (typeof destinationVersionBefore === "string" || destinationVersionBefore === null) && isRecord(receipt) ? {
60
+ txId,
61
+ path: committedPath,
62
+ requestHash,
63
+ toVersion,
64
+ destinationVersionBefore,
65
+ receipt
66
+ } : null;
67
+ };
68
+ const isDiscardRecord = (line, txId) => {
69
+ const parsed = Result.try(() => JSON.parse(line));
70
+ return parsed.isOk() && isRecord(parsed.value) && parsed.value["type"] === "recovery" && parsed.value["txId"] === txId && parsed.value["action"] === "discarded";
71
+ };
72
+ /**
73
+ * The committed transaction with `txId`, if the journal has one and recovery
74
+ * did not discard it. Torn or foreign lines (a crash mid-append) are skipped.
75
+ * A journal (or `.folio`) that is a symlink or hard link is refused.
76
+ */
77
+ const findCommit = async (journalPath, txId) => {
78
+ const journal = await inspectPath(journalPath);
79
+ if (journal.isErr()) return Result.err(journal.error);
80
+ if (journal.value.type === "missing") {
81
+ const parent = await inspectPath(path.dirname(journalPath));
82
+ if (parent.isErr()) return Result.err(parent.error);
83
+ return parent.value.type === "missing" || parent.value.type === "directory" ? Result.ok(void 0) : Result.err(cliError({
84
+ code: FOLIO_CLI_ERROR_CODES.unsafePath,
85
+ message: `${path.dirname(journalPath)} is not a plain directory.`
86
+ }));
87
+ }
88
+ const parent = await ensureSidecarParent(journalPath);
89
+ if (parent.isErr()) return Result.err(parent.error);
90
+ const bytes = await readSidecarFile(journalPath);
91
+ if (bytes.isErr()) return Result.err(bytes.error);
92
+ let found;
93
+ for (const line of new TextDecoder().decode(bytes.value).split("\n")) {
94
+ if (!line.includes(txId)) continue;
95
+ if (isDiscardRecord(line, txId)) return Result.ok(void 0);
96
+ const commit = parseCommit(line);
97
+ if (commit?.txId === txId) found ??= commit;
98
+ }
99
+ return Result.ok(found);
100
+ };
101
+ const stagePrefixFor = (documentPath) => `.${path.basename(documentPath)}.folio-stage-`;
102
+ const stagePathFor = (documentPath, txId) => path.join(path.dirname(documentPath), `${stagePrefixFor(documentPath)}${txId}`);
103
+ /** The destination's current version, read without following a symlink; `null` when missing. */
104
+ const destinationVersion = async (documentPath) => {
105
+ const entry = await inspectPath(documentPath);
106
+ if (entry.isErr()) return Result.err(entry.error);
107
+ if (entry.value.type === "missing") return Result.ok(null);
108
+ const bytes = await readSidecarFile(documentPath);
109
+ return bytes.isErr() ? Result.err(bytes.error) : Result.ok(fileVersionOf(bytes.value));
110
+ };
111
+ /**
112
+ * Whether a journaled stage may complete its rename: its line names this
113
+ * document, it is a regular single-link file (a symlink is never followed),
114
+ * its bytes are the version the line recorded and still parse as a package,
115
+ * and the destination is still what the transaction replaced.
116
+ */
117
+ const judgeStage = async ({ documentPath, stagePath, commit }) => {
118
+ if (commit.path !== documentPath) return Result.ok({
119
+ type: "discard",
120
+ reason: "otherDocument"
121
+ });
122
+ const entry = await inspectPath(stagePath);
123
+ if (entry.isErr()) return Result.err(entry.error);
124
+ if (entry.value.type !== "file" || entry.value.links !== 1) return Result.ok({
125
+ type: "discard",
126
+ reason: "notRegularFile"
127
+ });
128
+ const opened = await openSidecarFile(stagePath, constants.O_RDONLY);
129
+ if (opened.isErr()) return Result.ok({
130
+ type: "discard",
131
+ reason: "notRegularFile"
132
+ });
133
+ const { handle, identity } = opened.value;
134
+ const read = await Result.tryPromise(async () => new Uint8Array(await handle.readFile()));
135
+ await handle.close();
136
+ if (read.isErr() || !sameFile(identity, entry.value.identity)) return Result.ok({
137
+ type: "discard",
138
+ reason: "stageChanged"
139
+ });
140
+ if (fileVersionOf(read.value) !== commit.toVersion) return Result.ok({
141
+ type: "discard",
142
+ reason: "stageChanged"
143
+ });
144
+ if ((await checkPackageIntegrity(read.value, [])).isErr()) return Result.ok({
145
+ type: "discard",
146
+ reason: "invalidPackage"
147
+ });
148
+ const current = await destinationVersion(documentPath);
149
+ if (current.isErr()) return Result.err(current.error);
150
+ return current.value === commit.destinationVersionBefore ? Result.ok({ type: "rollForward" }) : Result.ok({
151
+ type: "discard",
152
+ reason: "destinationMoved"
153
+ });
154
+ };
155
+ /**
156
+ * Settle every stage a crashed transaction left beside `documentPath`. The
157
+ * caller holds the document's lease, so no live transaction owns a stage.
158
+ * Removing a discarded stage unlinks the name only; a symlink's target is
159
+ * never touched.
160
+ */
161
+ const recoverStages = async ({ documentPath, journalPath, now }) => {
162
+ const directory = path.dirname(documentPath);
163
+ const prefix = stagePrefixFor(documentPath);
164
+ const listed = await Result.tryPromise(() => readdir(directory));
165
+ if (listed.isErr()) return errnoCode(listed.error.cause) === "ENOENT" ? Result.ok([]) : Result.err(journalError(`Cannot list ${directory}`, listed.error.cause));
166
+ const actions = [];
167
+ for (const stage of listed.value.filter((name) => name.startsWith(prefix))) {
168
+ const txId = stage.slice(prefix.length);
169
+ const stagePath = path.join(directory, stage);
170
+ const commit = await findCommit(journalPath, txId);
171
+ if (commit.isErr()) return Result.err(commit.error);
172
+ if (commit.value === void 0) {
173
+ await rm(stagePath, { force: true });
174
+ continue;
175
+ }
176
+ const verdict = await judgeStage({
177
+ documentPath,
178
+ stagePath,
179
+ commit: commit.value
180
+ });
181
+ if (verdict.isErr()) return Result.err(verdict.error);
182
+ let action;
183
+ if (verdict.value.type === "rollForward") {
184
+ const renamed = await Result.tryPromise(() => rename(stagePath, documentPath));
185
+ if (renamed.isErr()) return Result.err(journalError(`Cannot complete ${stage}`, renamed.error.cause));
186
+ const synced = await syncDirectory(directory);
187
+ if (synced.isErr()) return Result.err(synced.error);
188
+ action = {
189
+ txId,
190
+ action: "rolledForward"
191
+ };
192
+ } else {
193
+ await rm(stagePath, { force: true });
194
+ action = {
195
+ txId,
196
+ action: "discarded",
197
+ reason: verdict.value.reason
198
+ };
199
+ }
200
+ const recorded = await appendJournal(journalPath, {
201
+ type: "recovery",
202
+ path: documentPath,
203
+ time: now,
204
+ ...action
205
+ });
206
+ if (recorded.isErr()) return Result.err(recorded.error);
207
+ actions.push(action);
208
+ }
209
+ return Result.ok(actions);
210
+ };
211
+ //#endregion
212
+ export { RECOVERY_DISCARD_REASONS, appendJournal, findCommit, journalPathFor, recoverStages, stagePathFor, stagePrefixFor };
package/dist/lock.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ import { FolioCliError } from "./errors.js";
2
+ import { Result } from "better-result";
3
+ //#region src/lock.d.ts
4
+ /** How long a command-line transaction's lease lasts without renewal. */
5
+ declare const TRANSACTION_LEASE_MS: number;
6
+ type LockHolder = {
7
+ owner: string;
8
+ pid: number;
9
+ host: string;
10
+ txId: string;
11
+ /** Random per acquisition; the fencing check compares it. */
12
+ token: string;
13
+ acquiredAt: string;
14
+ expiresAt: string;
15
+ };
16
+ declare const lockPathFor: (documentPath: string) => string;
17
+ /** What is beside the document: no lock, or a lock with its parsed holder when it has one. */
18
+ type LeaseState = {
19
+ type: "free";
20
+ } | {
21
+ type: "held";
22
+ holder: LockHolder;
23
+ modifiedMs: number;
24
+ } | {
25
+ type: "unreadable";
26
+ modifiedMs: number;
27
+ };
28
+ /**
29
+ * Whether a lock no longer protects anything. One that cannot be read is
30
+ * held until it is older than a whole lease, so a lock another process is
31
+ * writing is never taken for abandoned.
32
+ */
33
+ declare const isStaleLease: (state: LeaseState, now: Date) => boolean;
34
+ /** The lease currently beside `documentPath`. A symlinked lock reads as unreadable. */
35
+ declare const readLease: (documentPath: string) => Promise<LeaseState>;
36
+ type AcquiredLease = {
37
+ holder: LockHolder;
38
+ /** Refuse unless the lock still carries this lease's token. */
39
+ verify: () => Promise<Result<void, FolioCliError>>;
40
+ release: () => Promise<void>;
41
+ };
42
+ type AcquireLeaseOptions = {
43
+ documentPath: string;
44
+ txId: string;
45
+ /** Take the lease over even when another live holder has it. */
46
+ force: boolean;
47
+ now?: Date;
48
+ };
49
+ /**
50
+ * Take the lease for one transaction. A stale lease is replaced; a live or
51
+ * unreadable one refuses with `locked` unless `force`.
52
+ */
53
+ declare const acquireLease: ({ documentPath, txId, force, now }: AcquireLeaseOptions) => Promise<Result<AcquiredLease, FolioCliError>>;
54
+ //#endregion
55
+ export { AcquiredLease, LeaseState, LockHolder, TRANSACTION_LEASE_MS, acquireLease, isStaleLease, lockPathFor, readLease };
package/dist/lock.js ADDED
@@ -0,0 +1,157 @@
1
+ import { errnoCode } from "./document.js";
2
+ import { FOLIO_CLI_ERROR_CODES, cliError } from "./errors.js";
3
+ import { inspectPath, readSidecarFile, writeNewSidecarFile } from "./sidecar.js";
4
+ import { Result } from "better-result";
5
+ import { randomUUID } from "node:crypto";
6
+ import { link, rename, rm } from "node:fs/promises";
7
+ import path from "node:path";
8
+ import { hostname } from "node:os";
9
+ //#region src/lock.ts
10
+ /**
11
+ * Cooperative write lease: `.<name>.folio-lock` beside the document, holding
12
+ * the owner's pid, host, expiry, and a random token. A folio write takes it
13
+ * for the whole transaction and checks, just before it journals and renames,
14
+ * that the lock still carries its token; a writer whose lease was taken over
15
+ * stops there. A long-lived holder (an editor session) renews it.
16
+ *
17
+ * The lock appears complete or not at all: it is written to a private
18
+ * temporary file and hard-linked into place, which fails when a lock exists.
19
+ * A lease is stale once it expires or, on the same host, once its process has
20
+ * exited. A lock that cannot be parsed is treated as held until it is older
21
+ * than a lease. Reads never consult it.
22
+ */
23
+ /** How long a command-line transaction's lease lasts without renewal. */
24
+ const TRANSACTION_LEASE_MS = 300 * 1e3;
25
+ const lockPathFor = (documentPath) => path.join(path.dirname(documentPath), `.${path.basename(documentPath)}.folio-lock`);
26
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
27
+ const parseHolder = (text) => {
28
+ const parsed = Result.try(() => JSON.parse(text));
29
+ if (parsed.isErr() || !isRecord(parsed.value)) return null;
30
+ const { owner, pid, host, txId, token, acquiredAt, expiresAt } = parsed.value;
31
+ return typeof owner === "string" && typeof pid === "number" && typeof host === "string" && typeof txId === "string" && typeof token === "string" && typeof acquiredAt === "string" && typeof expiresAt === "string" ? {
32
+ owner,
33
+ pid,
34
+ host,
35
+ txId,
36
+ token,
37
+ acquiredAt,
38
+ expiresAt
39
+ } : null;
40
+ };
41
+ const processIsAlive = (pid) => {
42
+ const probe = Result.try(() => process.kill(pid, 0));
43
+ return probe.isOk() || errnoCode(probe.error.cause) === "EPERM";
44
+ };
45
+ /**
46
+ * Whether a lock no longer protects anything. One that cannot be read is
47
+ * held until it is older than a whole lease, so a lock another process is
48
+ * writing is never taken for abandoned.
49
+ */
50
+ const isStaleLease = (state, now) => {
51
+ switch (state.type) {
52
+ case "free": return true;
53
+ case "unreadable": return state.modifiedMs + TRANSACTION_LEASE_MS <= now.getTime();
54
+ case "held": {
55
+ const expires = Date.parse(state.holder.expiresAt);
56
+ if (Number.isNaN(expires) || expires <= now.getTime()) return true;
57
+ return state.holder.host === hostname() && !processIsAlive(state.holder.pid);
58
+ }
59
+ default: return state;
60
+ }
61
+ };
62
+ /** The lease currently beside `documentPath`. A symlinked lock reads as unreadable. */
63
+ const readLease = async (documentPath) => {
64
+ const lockPath = lockPathFor(documentPath);
65
+ const entry = await inspectPath(lockPath);
66
+ if (entry.isErr()) return {
67
+ type: "unreadable",
68
+ modifiedMs: Date.now()
69
+ };
70
+ if (entry.value.type === "missing") return { type: "free" };
71
+ const modifiedMs = entry.value.type === "file" ? entry.value.modifiedMs : Date.now();
72
+ const bytes = await readSidecarFile(lockPath);
73
+ const holder = bytes.isOk() ? parseHolder(new TextDecoder().decode(bytes.value)) : null;
74
+ return holder === null ? {
75
+ type: "unreadable",
76
+ modifiedMs
77
+ } : {
78
+ type: "held",
79
+ holder,
80
+ modifiedMs
81
+ };
82
+ };
83
+ const lockedError = (documentPath, state) => cliError({
84
+ code: FOLIO_CLI_ERROR_CODES.locked,
85
+ message: `${documentPath} is being written by another process${state.type === "held" ? ` (${state.holder.owner}, pid ${state.holder.pid} on ${state.holder.host})` : ""}.`,
86
+ hint: "Retry after it finishes, or pass --force to take the lease over.",
87
+ details: state.type === "held" ? { holder: state.holder } : void 0
88
+ });
89
+ const fileSystemError = (message, error) => cliError({
90
+ code: FOLIO_CLI_ERROR_CODES.internal,
91
+ message: `${message}: ${error instanceof Error ? error.message : String(error)}`
92
+ });
93
+ /**
94
+ * Put a complete lock in place: write it to a private temporary file, then
95
+ * `link` it to the lock path (fails when one exists) or, taking over,
96
+ * `rename` it there. The lock path never names a partly written file.
97
+ */
98
+ const placeLock = async (lockPath, contents, mode) => {
99
+ const temporary = `${lockPath}.${randomUUID()}.tmp`;
100
+ const written = await writeNewSidecarFile(temporary, contents);
101
+ if (written.isErr()) return Result.err(written.error);
102
+ try {
103
+ const placed = await Result.tryPromise(() => mode === "create" ? link(temporary, lockPath) : rename(temporary, lockPath));
104
+ if (placed.isOk()) return Result.ok({ type: "placed" });
105
+ return errnoCode(placed.error.cause) === "EEXIST" ? Result.ok({ type: "exists" }) : Result.err(fileSystemError(`Cannot create ${lockPath}`, placed.error.cause));
106
+ } finally {
107
+ await rm(temporary, { force: true });
108
+ }
109
+ };
110
+ /**
111
+ * Take the lease for one transaction. A stale lease is replaced; a live or
112
+ * unreadable one refuses with `locked` unless `force`.
113
+ */
114
+ const acquireLease = async ({ documentPath, txId, force, now = /* @__PURE__ */ new Date() }) => {
115
+ const lockPath = lockPathFor(documentPath);
116
+ const holder = {
117
+ owner: "folio-cli",
118
+ pid: process.pid,
119
+ host: hostname(),
120
+ txId,
121
+ token: randomUUID(),
122
+ acquiredAt: now.toISOString(),
123
+ expiresAt: new Date(now.getTime() + TRANSACTION_LEASE_MS).toISOString()
124
+ };
125
+ const contents = new TextEncoder().encode(`${JSON.stringify(holder)}\n`);
126
+ const holds = async () => {
127
+ const current = await readLease(documentPath);
128
+ return current.type === "held" && current.holder.token === holder.token;
129
+ };
130
+ const lease = {
131
+ holder,
132
+ verify: async () => await holds() ? Result.ok() : Result.err(cliError({
133
+ code: FOLIO_CLI_ERROR_CODES.locked,
134
+ message: `Another process took over the write lease on ${documentPath}; nothing was written.`,
135
+ hint: "Re-read the document and retry."
136
+ })),
137
+ release: async () => {
138
+ if (await holds()) await rm(lockPath, { force: true });
139
+ }
140
+ };
141
+ for (let attempt = 0; attempt < 2; attempt++) {
142
+ const placed = await placeLock(lockPath, contents, "create");
143
+ if (placed.isErr()) return Result.err(placed.error);
144
+ if (placed.value.type === "placed") return Result.ok(lease);
145
+ const existing = await readLease(documentPath);
146
+ if (force) {
147
+ const replaced = await placeLock(lockPath, contents, "replace");
148
+ if (replaced.isErr()) return Result.err(replaced.error);
149
+ return Result.ok(lease);
150
+ }
151
+ if (!isStaleLease(existing, now)) return Result.err(lockedError(documentPath, existing));
152
+ await rm(lockPath, { force: true });
153
+ }
154
+ return Result.err(lockedError(documentPath, { type: "free" }));
155
+ };
156
+ //#endregion
157
+ export { TRANSACTION_LEASE_MS, acquireLease, isStaleLease, lockPathFor, readLease };
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ import { FolioFileToolSpec, JsonObjectSchema } from "./registry.js";
2
+ import { FolioReadBounds } from "./execute-read.js";
3
+ import { AllowedRoots } from "./roots.js";
4
+ import { Readable, Writable } from "node:stream";
5
+ import { Server, Tool } from "@modelcontextprotocol/server";
6
+ //#region src/mcp.d.ts
7
+ /** Limits on what one MCP tool call returns. */
8
+ declare const MCP_READ_BOUNDS: FolioReadBounds;
9
+ type FolioMcpServerOptions = {
10
+ roots: AllowedRoots;
11
+ /** Author for every change; `undefined` refuses writes with `author_required`. */
12
+ author: string | undefined;
13
+ bounds?: FolioReadBounds;
14
+ now?: () => Date;
15
+ };
16
+ /** The tool's MCP input schema: its own arguments plus the file envelope. */
17
+ declare const mcpInputSchema: (tool: FolioFileToolSpec) => JsonObjectSchema;
18
+ declare const listMcpTools: () => Tool[];
19
+ /** A low-level MCP server exposing the file tools; connect it to a transport. */
20
+ declare const createFolioMcpServer: (options: FolioMcpServerOptions) => Server;
21
+ type ServeOptions = FolioMcpServerOptions & {
22
+ input: Readable;
23
+ output: Writable;
24
+ };
25
+ /** Serve the file tools over stdio until the client disconnects. */
26
+ declare const serveFolioMcp: ({ input, output, ...options }: ServeOptions) => Promise<void>;
27
+ //#endregion
28
+ export { FolioMcpServerOptions, MCP_READ_BOUNDS, createFolioMcpServer, listMcpTools, mcpInputSchema, serveFolioMcp };