@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.
- package/README.md +298 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +30 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +440 -0
- package/dist/document.d.ts +43 -0
- package/dist/document.js +127 -0
- package/dist/errors.d.ts +67 -0
- package/dist/errors.js +92 -0
- package/dist/execute-read.d.ts +64 -0
- package/dist/execute-read.js +220 -0
- package/dist/execute-write.d.ts +36 -0
- package/dist/execute-write.js +449 -0
- package/dist/flags.d.ts +38 -0
- package/dist/flags.js +102 -0
- package/dist/journal.d.ts +82 -0
- package/dist/journal.js +212 -0
- package/dist/lock.d.ts +55 -0
- package/dist/lock.js +157 -0
- package/dist/mcp.d.ts +28 -0
- package/dist/mcp.js +310 -0
- package/dist/output-file.d.ts +14 -0
- package/dist/output-file.js +60 -0
- package/dist/output.d.ts +33 -0
- package/dist/output.js +77 -0
- package/dist/package-parts.d.ts +24 -0
- package/dist/package-parts.js +172 -0
- package/dist/package.js +4 -0
- package/dist/preview-commands.d.ts +8 -0
- package/dist/preview-commands.js +226 -0
- package/dist/provenance.d.ts +17 -0
- package/dist/provenance.js +53 -0
- package/dist/registry.d.ts +69 -0
- package/dist/registry.js +255 -0
- package/dist/render.d.ts +35 -0
- package/dist/render.js +120 -0
- package/dist/roots.d.ts +21 -0
- package/dist/roots.js +57 -0
- package/dist/serve.d.ts +17 -0
- package/dist/serve.js +234 -0
- package/dist/sidecar.d.ts +52 -0
- package/dist/sidecar.js +156 -0
- package/dist/transaction.d.ts +55 -0
- package/dist/transaction.js +218 -0
- package/package.json +85 -3
package/dist/errors.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { TaggedError, panic } from "better-result";
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* Process exit classes. The numbers match the stella command line's table so
|
|
5
|
+
* a script driving both branches on one set of codes; folio uses the subset a
|
|
6
|
+
* local file tool can produce.
|
|
7
|
+
*/
|
|
8
|
+
const EXIT_CODES = {
|
|
9
|
+
ok: 0,
|
|
10
|
+
unexpected: 1,
|
|
11
|
+
validation: 2,
|
|
12
|
+
notFound: 6,
|
|
13
|
+
permissionDenied: 8,
|
|
14
|
+
conflict: 10
|
|
15
|
+
};
|
|
16
|
+
const EXIT_CODE_DESCRIPTIONS = {
|
|
17
|
+
ok: "success",
|
|
18
|
+
unexpected: "unexpected internal error",
|
|
19
|
+
validation: "usage, input, or refused-operation error",
|
|
20
|
+
notFound: "file, change, or comment not found",
|
|
21
|
+
permissionDenied: "path outside the allowed roots, or unsafe to write through",
|
|
22
|
+
conflict: "conflict with current state (stale version, lock held, destination exists)"
|
|
23
|
+
};
|
|
24
|
+
/** Every exit code with its meaning, ordered numerically, for `--help`. */
|
|
25
|
+
const exitCodeEntries = () => {
|
|
26
|
+
const descriptions = EXIT_CODE_DESCRIPTIONS;
|
|
27
|
+
return Object.entries(EXIT_CODES).toSorted(([, left], [, right]) => left - right).map(([key, code]) => ({
|
|
28
|
+
code,
|
|
29
|
+
meaning: descriptions[key] ?? panic(`exit code ${key} has no description`)
|
|
30
|
+
}));
|
|
31
|
+
};
|
|
32
|
+
/** Machine codes carried in `error.code` of a failure envelope. */
|
|
33
|
+
const FOLIO_CLI_ERROR_CODES = {
|
|
34
|
+
usage: "usage_error",
|
|
35
|
+
invalidInput: "invalid_input",
|
|
36
|
+
invalidDocument: "invalid_document",
|
|
37
|
+
tooLarge: "too_large",
|
|
38
|
+
notFound: "not_found",
|
|
39
|
+
outsideRoot: "outside_root",
|
|
40
|
+
unsafePath: "unsafe_path",
|
|
41
|
+
invalidDestination: "invalid_destination",
|
|
42
|
+
staleVersion: "stale_version",
|
|
43
|
+
staleTarget: "stale_target",
|
|
44
|
+
ambiguousTarget: "ambiguous_target",
|
|
45
|
+
operationRejected: "operation_rejected",
|
|
46
|
+
repackRequired: "repack_required",
|
|
47
|
+
authorRequired: "author_required",
|
|
48
|
+
locked: "locked",
|
|
49
|
+
destinationExists: "destination_exists",
|
|
50
|
+
transactionConflict: "transaction_conflict",
|
|
51
|
+
integrityFailed: "integrity_failed",
|
|
52
|
+
rendererUnavailable: "renderer_unavailable",
|
|
53
|
+
internal: "internal_error"
|
|
54
|
+
};
|
|
55
|
+
const ERROR_CODE_EXIT = {
|
|
56
|
+
usage_error: EXIT_CODES.validation,
|
|
57
|
+
invalid_input: EXIT_CODES.validation,
|
|
58
|
+
invalid_document: EXIT_CODES.validation,
|
|
59
|
+
too_large: EXIT_CODES.validation,
|
|
60
|
+
not_found: EXIT_CODES.notFound,
|
|
61
|
+
outside_root: EXIT_CODES.permissionDenied,
|
|
62
|
+
unsafe_path: EXIT_CODES.permissionDenied,
|
|
63
|
+
invalid_destination: EXIT_CODES.validation,
|
|
64
|
+
stale_version: EXIT_CODES.conflict,
|
|
65
|
+
stale_target: EXIT_CODES.conflict,
|
|
66
|
+
ambiguous_target: EXIT_CODES.validation,
|
|
67
|
+
operation_rejected: EXIT_CODES.validation,
|
|
68
|
+
repack_required: EXIT_CODES.validation,
|
|
69
|
+
author_required: EXIT_CODES.validation,
|
|
70
|
+
locked: EXIT_CODES.conflict,
|
|
71
|
+
destination_exists: EXIT_CODES.conflict,
|
|
72
|
+
transaction_conflict: EXIT_CODES.conflict,
|
|
73
|
+
integrity_failed: EXIT_CODES.unexpected,
|
|
74
|
+
renderer_unavailable: EXIT_CODES.validation,
|
|
75
|
+
internal_error: EXIT_CODES.unexpected
|
|
76
|
+
};
|
|
77
|
+
const exitCodeForError = (code) => ERROR_CODE_EXIT[code];
|
|
78
|
+
/**
|
|
79
|
+
* Every expected failure of a command or tool call. `hint` names the next
|
|
80
|
+
* step (re-read, pass a flag); `details` carries structured context such as
|
|
81
|
+
* the skipped operations of a refused batch.
|
|
82
|
+
*/
|
|
83
|
+
var FolioCliError = class extends TaggedError("FolioCliError") {};
|
|
84
|
+
/** Build a {@link FolioCliError} without spelling absent optional fields. */
|
|
85
|
+
const cliError = ({ code, message, hint, details }) => new FolioCliError({
|
|
86
|
+
code,
|
|
87
|
+
message,
|
|
88
|
+
...hint !== void 0 && { hint },
|
|
89
|
+
...details !== void 0 && { details }
|
|
90
|
+
});
|
|
91
|
+
//#endregion
|
|
92
|
+
export { EXIT_CODES, FOLIO_CLI_ERROR_CODES, FolioCliError, cliError, exitCodeEntries, exitCodeForError };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { FolioCliError } from "./errors.js";
|
|
2
|
+
import { FolioFileToolSpec } from "./registry.js";
|
|
3
|
+
import { Result } from "better-result";
|
|
4
|
+
//#region src/execute-read.d.ts
|
|
5
|
+
/** One call against one file: the envelope plus the tool's own arguments. */
|
|
6
|
+
type FileToolCall = {
|
|
7
|
+
path: string;
|
|
8
|
+
/** When present, the call is refused unless the file still has this version. */
|
|
9
|
+
fileVersion?: string | undefined;
|
|
10
|
+
args: Readonly<Record<string, unknown>>;
|
|
11
|
+
};
|
|
12
|
+
/** Per-surface limits on what one read returns. */
|
|
13
|
+
type FolioReadBounds = {
|
|
14
|
+
/** Page size when `read_document` passes none; `null` returns every block. */
|
|
15
|
+
defaultMaxBlocks: number | null;
|
|
16
|
+
/** Most `find_text` matches returned; the rest are counted, not listed. */
|
|
17
|
+
maxMatches: number;
|
|
18
|
+
/** Largest serialized result; `null` for no limit. */
|
|
19
|
+
maxResponseBytes: number | null;
|
|
20
|
+
};
|
|
21
|
+
declare const CLI_READ_BOUNDS: FolioReadBounds;
|
|
22
|
+
/** The data every file read returns: which file, at which version, and the result. */
|
|
23
|
+
type FileReadData = {
|
|
24
|
+
path: string;
|
|
25
|
+
fileVersion: string;
|
|
26
|
+
result: unknown;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Where a block id comes from. `package` ids are the paragraph's own
|
|
30
|
+
* `w14:paraId`; `synthetic` ids are derived from the paragraph's text and
|
|
31
|
+
* position, valid only for the fileVersion they were read at.
|
|
32
|
+
*/
|
|
33
|
+
type FolioBlockIdSource = "package" | "synthetic";
|
|
34
|
+
type BlockIdSources = ReadonlyMap<string, FolioBlockIdSource>;
|
|
35
|
+
type ReadCursor = {
|
|
36
|
+
fileVersion: string;
|
|
37
|
+
afterBlockId: string;
|
|
38
|
+
};
|
|
39
|
+
declare const encodeReadCursor: ({ fileVersion, afterBlockId }: ReadCursor) => string;
|
|
40
|
+
type ReadDocumentPage = {
|
|
41
|
+
blocks: unknown[];
|
|
42
|
+
totalBlocks: number;
|
|
43
|
+
truncated: boolean;
|
|
44
|
+
nextCursor?: string;
|
|
45
|
+
};
|
|
46
|
+
type PageReadDocumentOptions = {
|
|
47
|
+
blocks: readonly unknown[];
|
|
48
|
+
sources: BlockIdSources;
|
|
49
|
+
fileVersion: string;
|
|
50
|
+
args: Readonly<Record<string, unknown>>;
|
|
51
|
+
bounds: FolioReadBounds;
|
|
52
|
+
/** Bytes the page's blocks may take; `null` for no limit. */
|
|
53
|
+
pageByteBudget: number | null;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Page `read_document`'s blocks by count and serialized size. A cursor names
|
|
57
|
+
* the last block of the previous page and the version it was read at, so a
|
|
58
|
+
* page from one version never continues on another.
|
|
59
|
+
*/
|
|
60
|
+
declare const pageReadDocument: ({ blocks, sources, fileVersion, args, bounds, pageByteBudget }: PageReadDocumentOptions) => Result<ReadDocumentPage, FolioCliError>;
|
|
61
|
+
/** Run one read tool, or a compare without a destination. Never writes. */
|
|
62
|
+
declare const executeReadTool: (tool: FolioFileToolSpec, call: FileToolCall, bounds: FolioReadBounds) => Promise<Result<FileReadData, FolioCliError>>;
|
|
63
|
+
//#endregion
|
|
64
|
+
export { CLI_READ_BOUNDS, FileReadData, FileToolCall, FolioBlockIdSource, FolioReadBounds, encodeReadCursor, executeReadTool, pageReadDocument };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { checkExpectedVersion, openReviewer, readDocumentFile } from "./document.js";
|
|
2
|
+
import { FOLIO_CLI_ERROR_CODES, cliError } from "./errors.js";
|
|
3
|
+
import { MAX_READ_BLOCKS } from "./registry.js";
|
|
4
|
+
import { Result, panic } from "better-result";
|
|
5
|
+
import { createReviewerBridge } from "@stll/folio-agents/bridges/reviewer";
|
|
6
|
+
import { compareDocxVersions, formatVersionDiffForLLM } from "@stll/folio-agents/compare";
|
|
7
|
+
import { executeFolioToolCallUntyped } from "@stll/folio-agents/execute";
|
|
8
|
+
import { FOLIO_AGENT_TOOL_NAMES } from "@stll/folio-agents/types";
|
|
9
|
+
//#region src/execute-read.ts
|
|
10
|
+
const CLI_READ_BOUNDS = {
|
|
11
|
+
defaultMaxBlocks: null,
|
|
12
|
+
maxMatches: 200,
|
|
13
|
+
maxResponseBytes: null
|
|
14
|
+
};
|
|
15
|
+
const idSourceOf = ({ idStability }) => idStability === "positional" ? "synthetic" : "package";
|
|
16
|
+
const blockIdSources = (reviewer) => new Map(reviewer.snapshot().blocks.map((block) => [block.id, idSourceOf(block)]));
|
|
17
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
const blockIdOf = (block) => isRecord(block) && typeof block["blockId"] === "string" ? block["blockId"] : void 0;
|
|
19
|
+
/** Label each block of a read with where its id comes from. */
|
|
20
|
+
const labelBlocks = (blocks, sources) => blocks.map((block) => {
|
|
21
|
+
const blockId = blockIdOf(block);
|
|
22
|
+
if (blockId === void 0 || !isRecord(block)) return block;
|
|
23
|
+
return {
|
|
24
|
+
...block,
|
|
25
|
+
blockIdSource: sources.get(blockId) ?? "synthetic"
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
const encodeReadCursor = ({ fileVersion, afterBlockId }) => Buffer.from(JSON.stringify({
|
|
29
|
+
v: fileVersion,
|
|
30
|
+
a: afterBlockId
|
|
31
|
+
}), "utf8").toString("base64url");
|
|
32
|
+
const decodeReadCursor = (cursor) => {
|
|
33
|
+
const parsed = Result.try(() => JSON.parse(Buffer.from(cursor, "base64url").toString("utf8")));
|
|
34
|
+
if (parsed.isErr() || !isRecord(parsed.value)) return null;
|
|
35
|
+
const { v, a } = parsed.value;
|
|
36
|
+
return typeof v === "string" && typeof a === "string" ? {
|
|
37
|
+
fileVersion: v,
|
|
38
|
+
afterBlockId: a
|
|
39
|
+
} : null;
|
|
40
|
+
};
|
|
41
|
+
const invalidInput = (message, hint) => cliError({
|
|
42
|
+
code: FOLIO_CLI_ERROR_CODES.invalidInput,
|
|
43
|
+
message,
|
|
44
|
+
hint
|
|
45
|
+
});
|
|
46
|
+
const byteLength = (value) => Buffer.byteLength(JSON.stringify(value) ?? "", "utf8");
|
|
47
|
+
/** Room kept in a paged response for its envelope fields and `nextCursor`. */
|
|
48
|
+
const PAGE_ENVELOPE_RESERVE_BYTES = 512;
|
|
49
|
+
/**
|
|
50
|
+
* Page `read_document`'s blocks by count and serialized size. A cursor names
|
|
51
|
+
* the last block of the previous page and the version it was read at, so a
|
|
52
|
+
* page from one version never continues on another.
|
|
53
|
+
*/
|
|
54
|
+
const pageReadDocument = ({ blocks, sources, fileVersion, args, bounds, pageByteBudget }) => {
|
|
55
|
+
const { maxBlocks, cursor } = args;
|
|
56
|
+
if (maxBlocks !== void 0 && (typeof maxBlocks !== "number" || !Number.isInteger(maxBlocks) || maxBlocks < 1 || maxBlocks > 1e3)) return Result.err(invalidInput(`maxBlocks must be an integer from 1 to ${MAX_READ_BLOCKS}.`));
|
|
57
|
+
let start = 0;
|
|
58
|
+
if (cursor !== void 0) {
|
|
59
|
+
const decoded = typeof cursor === "string" ? decodeReadCursor(cursor) : null;
|
|
60
|
+
if (decoded === null) return Result.err(invalidInput("cursor is not a nextCursor from read_document."));
|
|
61
|
+
if (decoded.fileVersion !== fileVersion) return Result.err(cliError({
|
|
62
|
+
code: FOLIO_CLI_ERROR_CODES.staleVersion,
|
|
63
|
+
message: "The document changed after this cursor was issued.",
|
|
64
|
+
hint: "Read again from the start, without a cursor.",
|
|
65
|
+
details: {
|
|
66
|
+
expected: decoded.fileVersion,
|
|
67
|
+
actual: fileVersion
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
const index = blocks.findIndex((block) => blockIdOf(block) === decoded.afterBlockId);
|
|
71
|
+
if (index === -1) return Result.err(invalidInput("cursor names a block this version does not have."));
|
|
72
|
+
start = index + 1;
|
|
73
|
+
}
|
|
74
|
+
const limit = typeof maxBlocks === "number" ? maxBlocks : bounds.defaultMaxBlocks;
|
|
75
|
+
const end = limit === null ? blocks.length : Math.min(blocks.length, start + limit);
|
|
76
|
+
const labelled = labelBlocks(blocks.slice(start, end), sources);
|
|
77
|
+
const page = [];
|
|
78
|
+
let bytes = 0;
|
|
79
|
+
for (const block of labelled) {
|
|
80
|
+
bytes += byteLength(block);
|
|
81
|
+
if (pageByteBudget !== null && bytes > pageByteBudget && page.length > 0) break;
|
|
82
|
+
page.push(block);
|
|
83
|
+
}
|
|
84
|
+
while (start + page.length < blocks.length && page.length > 0 && blockIdOf(page.at(-1)) === void 0) page.pop();
|
|
85
|
+
const lastBlockId = blockIdOf(page.at(-1));
|
|
86
|
+
const truncated = start + page.length < blocks.length;
|
|
87
|
+
if (truncated && lastBlockId === void 0) return Result.err(cliError({
|
|
88
|
+
code: FOLIO_CLI_ERROR_CODES.internal,
|
|
89
|
+
message: "read_document page has no block with an id; cannot issue a cursor."
|
|
90
|
+
}));
|
|
91
|
+
return Result.ok({
|
|
92
|
+
blocks: page,
|
|
93
|
+
totalBlocks: blocks.length,
|
|
94
|
+
truncated,
|
|
95
|
+
...truncated && lastBlockId !== void 0 && { nextCursor: encodeReadCursor({
|
|
96
|
+
fileVersion,
|
|
97
|
+
afterBlockId: lastBlockId
|
|
98
|
+
}) }
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
const unexpectedShape = (tool) => cliError({
|
|
102
|
+
code: FOLIO_CLI_ERROR_CODES.internal,
|
|
103
|
+
message: `${tool} returned an unexpected result shape.`
|
|
104
|
+
});
|
|
105
|
+
const runAgentRead = ({ tool, reviewer, fileVersion, args, bounds, pageByteBudget }) => {
|
|
106
|
+
const bridge = createReviewerBridge(reviewer);
|
|
107
|
+
const isReadDocument = tool.agentTool === FOLIO_AGENT_TOOL_NAMES.readDocument;
|
|
108
|
+
const agentArgs = isReadDocument ? {} : args;
|
|
109
|
+
const executed = executeFolioToolCallUntyped(tool.agentTool, agentArgs, bridge);
|
|
110
|
+
if (!executed.ok) return Result.err(invalidInput(executed.error));
|
|
111
|
+
const { result } = executed;
|
|
112
|
+
if (isReadDocument) {
|
|
113
|
+
if (!Array.isArray(result)) return Result.err(unexpectedShape(tool.name));
|
|
114
|
+
return pageReadDocument({
|
|
115
|
+
blocks: result,
|
|
116
|
+
sources: blockIdSources(reviewer),
|
|
117
|
+
fileVersion,
|
|
118
|
+
args,
|
|
119
|
+
bounds,
|
|
120
|
+
pageByteBudget
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (tool.agentTool === FOLIO_AGENT_TOOL_NAMES.readSection) {
|
|
124
|
+
if (!isRecord(result) || !Array.isArray(result["blocks"])) return Result.err(unexpectedShape(tool.name));
|
|
125
|
+
return Result.ok({
|
|
126
|
+
...result,
|
|
127
|
+
blocks: labelBlocks(result["blocks"], blockIdSources(reviewer))
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
if (tool.agentTool === FOLIO_AGENT_TOOL_NAMES.findText) {
|
|
131
|
+
if (!isRecord(result) || !Array.isArray(result["matches"])) return Result.err(unexpectedShape(tool.name));
|
|
132
|
+
const matches = result["matches"];
|
|
133
|
+
return Result.ok(matches.length <= bounds.maxMatches ? result : {
|
|
134
|
+
...result,
|
|
135
|
+
matches: matches.slice(0, bounds.maxMatches),
|
|
136
|
+
truncated: true
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return Result.ok(result);
|
|
140
|
+
};
|
|
141
|
+
const tooLarge = (tool, limit) => cliError({
|
|
142
|
+
code: FOLIO_CLI_ERROR_CODES.tooLarge,
|
|
143
|
+
message: `The ${tool} result exceeds the ${limit}-byte response limit.`,
|
|
144
|
+
hint: "Narrow the read: get_document_outline then read_section, a scoped find_text, or read_document paging."
|
|
145
|
+
});
|
|
146
|
+
/** Diff two files without writing: the structured diff plus a compact rendering. */
|
|
147
|
+
const compareFiles = async (tool, call, bounds) => {
|
|
148
|
+
const revisedPath = call.args["revisedPath"];
|
|
149
|
+
if (typeof revisedPath !== "string" || revisedPath === "") return Result.err(invalidInput("compare_documents needs revisedPath."));
|
|
150
|
+
const base = await readDocumentFile(call.path);
|
|
151
|
+
if (base.isErr()) return Result.err(base.error);
|
|
152
|
+
const baseVersion = checkExpectedVersion(base.value, call.fileVersion);
|
|
153
|
+
if (baseVersion.isErr()) return Result.err(baseVersion.error);
|
|
154
|
+
const revised = await readDocumentFile(revisedPath);
|
|
155
|
+
if (revised.isErr()) return Result.err(revised.error);
|
|
156
|
+
const expectedRevised = call.args["revisedFileVersion"];
|
|
157
|
+
const revisedVersion = checkExpectedVersion(revised.value, typeof expectedRevised === "string" ? expectedRevised : void 0);
|
|
158
|
+
if (revisedVersion.isErr()) return Result.err(revisedVersion.error);
|
|
159
|
+
const diff = await Result.tryPromise({
|
|
160
|
+
try: () => compareDocxVersions(base.value.bytes.slice().buffer, revised.value.bytes.slice().buffer),
|
|
161
|
+
catch: (error) => cliError({
|
|
162
|
+
code: FOLIO_CLI_ERROR_CODES.invalidDocument,
|
|
163
|
+
message: `The documents could not be compared: ${error instanceof Error ? error.message : String(error)}`
|
|
164
|
+
})
|
|
165
|
+
});
|
|
166
|
+
if (diff.isErr()) return Result.err(diff.error);
|
|
167
|
+
const data = {
|
|
168
|
+
path: base.value.path,
|
|
169
|
+
fileVersion: base.value.fileVersion,
|
|
170
|
+
revised: {
|
|
171
|
+
path: revised.value.path,
|
|
172
|
+
fileVersion: revised.value.fileVersion
|
|
173
|
+
},
|
|
174
|
+
result: {
|
|
175
|
+
text: formatVersionDiffForLLM(diff.value),
|
|
176
|
+
diff: diff.value
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
return bounds.maxResponseBytes !== null && byteLength(data) > bounds.maxResponseBytes ? Result.err(tooLarge(tool.name, bounds.maxResponseBytes)) : Result.ok(data);
|
|
180
|
+
};
|
|
181
|
+
const readWithAgentTool = async (tool, call, bounds) => {
|
|
182
|
+
const file = await readDocumentFile(call.path);
|
|
183
|
+
if (file.isErr()) return Result.err(file.error);
|
|
184
|
+
const version = checkExpectedVersion(file.value, call.fileVersion);
|
|
185
|
+
if (version.isErr()) return Result.err(version.error);
|
|
186
|
+
const reviewer = await openReviewer(file.value);
|
|
187
|
+
if (reviewer.isErr()) return Result.err(reviewer.error);
|
|
188
|
+
const { path, fileVersion } = file.value;
|
|
189
|
+
const result = runAgentRead({
|
|
190
|
+
tool,
|
|
191
|
+
reviewer: reviewer.value,
|
|
192
|
+
fileVersion,
|
|
193
|
+
args: call.args,
|
|
194
|
+
bounds,
|
|
195
|
+
pageByteBudget: bounds.maxResponseBytes === null ? null : bounds.maxResponseBytes - byteLength({
|
|
196
|
+
path,
|
|
197
|
+
fileVersion
|
|
198
|
+
}) - PAGE_ENVELOPE_RESERVE_BYTES
|
|
199
|
+
});
|
|
200
|
+
if (result.isErr()) return Result.err(result.error);
|
|
201
|
+
const data = {
|
|
202
|
+
path,
|
|
203
|
+
fileVersion,
|
|
204
|
+
result: result.value
|
|
205
|
+
};
|
|
206
|
+
if (bounds.maxResponseBytes !== null && byteLength(data) > bounds.maxResponseBytes) return Result.err(tooLarge(tool.name, bounds.maxResponseBytes));
|
|
207
|
+
return Result.ok(data);
|
|
208
|
+
};
|
|
209
|
+
/** Run one read tool, or a compare without a destination. Never writes. */
|
|
210
|
+
const executeReadTool = async (tool, call, bounds) => {
|
|
211
|
+
switch (tool.type) {
|
|
212
|
+
case "agentRead": return await readWithAgentTool(tool, call, bounds);
|
|
213
|
+
case "compare": return await compareFiles(tool, call, bounds);
|
|
214
|
+
case "agentWrite":
|
|
215
|
+
case "resolveChanges": return panic("A write tool reached the read executor", { tool: tool.name });
|
|
216
|
+
default: return panic("Unhandled tool type", { unreachable: tool });
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
//#endregion
|
|
220
|
+
export { CLI_READ_BOUNDS, encodeReadCursor, executeReadTool, pageReadDocument };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FolioCliError } from "./errors.js";
|
|
2
|
+
import { FolioFileToolSpec } from "./registry.js";
|
|
3
|
+
import { FileToolCall } from "./execute-read.js";
|
|
4
|
+
import { WriteDestination } from "./transaction.js";
|
|
5
|
+
import { Result } from "better-result";
|
|
6
|
+
import { FolioAIEditApplyMode } from "@stll/folio-core/server";
|
|
7
|
+
//#region src/execute-write.d.ts
|
|
8
|
+
/** Everything about a write that is not the tool's own arguments. */
|
|
9
|
+
type WriteOptions = {
|
|
10
|
+
destination: WriteDestination;
|
|
11
|
+
/** Already resolved: a write never proceeds without one. */
|
|
12
|
+
author: string;
|
|
13
|
+
/** The transaction's one UTC timestamp. */
|
|
14
|
+
date: string;
|
|
15
|
+
/** Whether a save the selective patch cannot express may rewrite every part. */
|
|
16
|
+
repack: "allow" | "refuse";
|
|
17
|
+
/** Take the lease over from another live holder. */
|
|
18
|
+
force: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* `required`: the call must name the source's fileVersion (every MCP
|
|
21
|
+
* write, and the CLI unless `--no-expect-version`). `waived`: explicitly
|
|
22
|
+
* skipped.
|
|
23
|
+
*/
|
|
24
|
+
sourcePrecondition: "required" | "waived";
|
|
25
|
+
/** Idempotency key; generated when absent. */
|
|
26
|
+
txId: string | undefined;
|
|
27
|
+
journalPath: string | undefined;
|
|
28
|
+
/** For tools whose edit mode is selectable. */
|
|
29
|
+
mode: FolioAIEditApplyMode;
|
|
30
|
+
};
|
|
31
|
+
/** How a transaction's bytes were produced. */
|
|
32
|
+
type SaveStrategy = "selective" | "full-repack" | "redline";
|
|
33
|
+
/** Run one write tool as a transaction and return its receipt. */
|
|
34
|
+
declare const executeWriteTool: (tool: FolioFileToolSpec, call: FileToolCall, options: WriteOptions) => Promise<Result<Readonly<Record<string, unknown>>, FolioCliError>>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { SaveStrategy, type WriteDestination, WriteOptions, executeWriteTool };
|