@better-fs-tools/read 0.0.0-tegami-trusted-publish-setup → 0.2.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/LICENSE +21 -0
- package/README.md +807 -6
- package/dist/authorize/ask-user.d.ts +7 -0
- package/dist/authorize/ask-user.js +25 -0
- package/dist/authorize/authorizers.d.ts +7 -0
- package/dist/authorize/authorizers.js +34 -0
- package/dist/authorize/deny-paths.d.ts +7 -0
- package/dist/authorize/deny-paths.js +25 -0
- package/dist/authorize/glob.d.ts +7 -0
- package/dist/authorize/glob.js +46 -0
- package/dist/authorize/index.d.ts +5 -0
- package/dist/authorize/index.js +5 -0
- package/dist/authorize/shared.d.ts +9 -0
- package/dist/authorize/shared.js +12 -0
- package/dist/authorize/size-ceiling.d.ts +10 -0
- package/dist/authorize/size-ceiling.js +34 -0
- package/dist/budget/chars-per-token.d.ts +9 -0
- package/dist/budget/chars-per-token.js +15 -0
- package/dist/budget/index.d.ts +2 -0
- package/dist/budget/index.js +1 -0
- package/dist/classifiers/binary.d.ts +5 -0
- package/dist/classifiers/binary.js +13 -0
- package/dist/classifiers/extension.d.ts +17 -0
- package/dist/classifiers/extension.js +45 -0
- package/dist/classifiers/image.d.ts +5 -0
- package/dist/classifiers/image.js +22 -0
- package/dist/classifiers/index.d.ts +16 -0
- package/dist/classifiers/index.js +31 -0
- package/dist/classifiers/notebook.d.ts +5 -0
- package/dist/classifiers/notebook.js +14 -0
- package/dist/classifiers/office.d.ts +5 -0
- package/dist/classifiers/office.js +30 -0
- package/dist/classifiers/pdf.d.ts +4 -0
- package/dist/classifiers/pdf.js +8 -0
- package/dist/classifiers/shared.d.ts +17 -0
- package/dist/classifiers/shared.js +57 -0
- package/dist/classifiers/utf8.d.ts +8 -0
- package/dist/classifiers/utf8.js +61 -0
- package/dist/contract/base.d.ts +126 -0
- package/dist/contract/base.js +0 -0
- package/dist/contract/classify.d.ts +50 -0
- package/dist/contract/classify.js +0 -0
- package/dist/contract/context.d.ts +13 -0
- package/dist/contract/context.js +0 -0
- package/dist/contract/deps.d.ts +60 -0
- package/dist/contract/deps.js +0 -0
- package/dist/contract/digest.d.ts +13 -0
- package/dist/contract/digest.js +0 -0
- package/dist/contract/extensions.d.ts +115 -0
- package/dist/contract/extensions.js +0 -0
- package/dist/contract/format.d.ts +16 -0
- package/dist/contract/format.js +0 -0
- package/dist/contract/input.d.ts +16 -0
- package/dist/contract/input.js +0 -0
- package/dist/contract/json.d.ts +6 -0
- package/dist/contract/json.js +0 -0
- package/dist/contract/limits.d.ts +20 -0
- package/dist/contract/limits.js +0 -0
- package/dist/contract/messages.d.ts +110 -0
- package/dist/contract/messages.js +0 -0
- package/dist/contract/result.d.ts +153 -0
- package/dist/contract/result.js +0 -0
- package/dist/contract/state.d.ts +36 -0
- package/dist/contract/state.js +0 -0
- package/dist/converters/directory-listing.d.ts +13 -0
- package/dist/converters/directory-listing.js +63 -0
- package/dist/converters/image.d.ts +7 -0
- package/dist/converters/image.js +25 -0
- package/dist/converters/index.d.ts +8 -0
- package/dist/converters/index.js +4 -0
- package/dist/converters/notebook.d.ts +12 -0
- package/dist/converters/notebook.js +106 -0
- package/dist/converters/shared.d.ts +5 -0
- package/dist/converters/shared.js +18 -0
- package/dist/converters/text.d.ts +12 -0
- package/dist/converters/text.js +24 -0
- package/dist/core/authorize.d.ts +8 -0
- package/dist/core/authorize.js +76 -0
- package/dist/core/budget.d.ts +7 -0
- package/dist/core/budget.js +23 -0
- package/dist/core/call-scope.d.ts +36 -0
- package/dist/core/call-scope.js +112 -0
- package/dist/core/classify.d.ts +9 -0
- package/dist/core/classify.js +27 -0
- package/dist/core/convert-source.d.ts +29 -0
- package/dist/core/convert-source.js +85 -0
- package/dist/core/convert.d.ts +23 -0
- package/dist/core/convert.js +132 -0
- package/dist/core/converted.d.ts +18 -0
- package/dist/core/converted.js +111 -0
- package/dist/core/create-read-tool.d.ts +7 -0
- package/dist/core/create-read-tool.js +18 -0
- package/dist/core/cursor.d.ts +14 -0
- package/dist/core/cursor.js +93 -0
- package/dist/core/deps.d.ts +6 -0
- package/dist/core/deps.js +121 -0
- package/dist/core/directory.d.ts +16 -0
- package/dist/core/directory.js +113 -0
- package/dist/core/extension-error.d.ts +9 -0
- package/dist/core/extension-error.js +18 -0
- package/dist/core/format.d.ts +8 -0
- package/dist/core/format.js +29 -0
- package/dist/core/hooks.d.ts +3 -0
- package/dist/core/hooks.js +140 -0
- package/dist/core/input.d.ts +10 -0
- package/dist/core/input.js +45 -0
- package/dist/core/limits.d.ts +9 -0
- package/dist/core/limits.js +43 -0
- package/dist/core/messages.d.ts +8 -0
- package/dist/core/messages.js +78 -0
- package/dist/core/notes.d.ts +14 -0
- package/dist/core/notes.js +94 -0
- package/dist/core/observation.d.ts +11 -0
- package/dist/core/observation.js +51 -0
- package/dist/core/open.d.ts +16 -0
- package/dist/core/open.js +33 -0
- package/dist/core/outcomes.d.ts +27 -0
- package/dist/core/outcomes.js +172 -0
- package/dist/core/pipeline.d.ts +4 -0
- package/dist/core/pipeline.js +175 -0
- package/dist/core/record.d.ts +3 -0
- package/dist/core/record.js +37 -0
- package/dist/core/resolve.d.ts +15 -0
- package/dist/core/resolve.js +43 -0
- package/dist/core/same.d.ts +2 -0
- package/dist/core/same.js +22 -0
- package/dist/core/sample.d.ts +6 -0
- package/dist/core/sample.js +24 -0
- package/dist/core/scan.d.ts +26 -0
- package/dist/core/scan.js +95 -0
- package/dist/core/scanner.d.ts +29 -0
- package/dist/core/scanner.js +124 -0
- package/dist/core/suggest.d.ts +6 -0
- package/dist/core/suggest.js +41 -0
- package/dist/core/text-outcome.d.ts +19 -0
- package/dist/core/text-outcome.js +72 -0
- package/dist/core/verify.d.ts +7 -0
- package/dist/core/verify.js +15 -0
- package/dist/digest/sha256.d.ts +7 -0
- package/dist/digest/sha256.js +131 -0
- package/dist/formats/deep-agents.d.ts +7 -0
- package/dist/formats/deep-agents.js +23 -0
- package/dist/formats/hashline.d.ts +6 -0
- package/dist/formats/hashline.js +16 -0
- package/dist/formats/hermes.d.ts +9 -0
- package/dist/formats/hermes.js +62 -0
- package/dist/formats/index.d.ts +4 -0
- package/dist/formats/index.js +4 -0
- package/dist/formats/opencode.d.ts +7 -0
- package/dist/formats/opencode.js +35 -0
- package/dist/formats/shared.d.ts +4 -0
- package/dist/formats/shared.js +9 -0
- package/dist/formatters/eof.d.ts +7 -0
- package/dist/formatters/eof.js +20 -0
- package/dist/formatters/file-hash.d.ts +7 -0
- package/dist/formatters/file-hash.js +13 -0
- package/dist/formatters/hashline.d.ts +11 -0
- package/dist/formatters/hashline.js +18 -0
- package/dist/formatters/index.d.ts +10 -0
- package/dist/formatters/index.js +7 -0
- package/dist/formatters/json.d.ts +17 -0
- package/dist/formatters/json.js +36 -0
- package/dist/formatters/layout.d.ts +15 -0
- package/dist/formatters/layout.js +33 -0
- package/dist/formatters/line-number.d.ts +11 -0
- package/dist/formatters/line-number.js +23 -0
- package/dist/formatters/plain.d.ts +7 -0
- package/dist/formatters/plain.js +14 -0
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/redact.d.ts +14 -0
- package/dist/hooks/redact.js +53 -0
- package/dist/hooks/repeat-read-guard.d.ts +13 -0
- package/dist/hooks/repeat-read-guard.js +49 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +15 -0
- package/dist/resolve/expand-home.d.ts +5 -0
- package/dist/resolve/expand-home.js +17 -0
- package/dist/resolve/index.d.ts +5 -0
- package/dist/resolve/index.js +5 -0
- package/dist/resolve/path-resolvers.d.ts +7 -0
- package/dist/resolve/path-resolvers.js +38 -0
- package/dist/resolve/reanchor-leading-slash.d.ts +9 -0
- package/dist/resolve/reanchor-leading-slash.js +23 -0
- package/dist/resolve/strip-prefixes.d.ts +6 -0
- package/dist/resolve/strip-prefixes.js +30 -0
- package/dist/resolve/unicode-repair.d.ts +9 -0
- package/dist/resolve/unicode-repair.js +38 -0
- package/dist/signature/contract.d.ts +18 -0
- package/dist/signature/contract.js +0 -0
- package/dist/signature/index.d.ts +4 -0
- package/dist/signature/index.js +3 -0
- package/dist/signature/line-range.d.ts +7 -0
- package/dist/signature/line-range.js +59 -0
- package/dist/signature/messages.d.ts +4 -0
- package/dist/signature/messages.js +9 -0
- package/dist/signature/offset-limit.d.ts +13 -0
- package/dist/signature/offset-limit.js +59 -0
- package/dist/signature/schema.d.ts +10 -0
- package/dist/signature/schema.js +80 -0
- package/dist/state/index.d.ts +15 -0
- package/dist/state/index.js +77 -0
- package/dist/suggest/canonicalize.d.ts +6 -0
- package/dist/suggest/canonicalize.js +12 -0
- package/dist/suggest/default-suggest.d.ts +3 -0
- package/dist/suggest/default-suggest.js +12 -0
- package/dist/suggest/index.d.ts +3 -0
- package/dist/suggest/index.js +3 -0
- package/dist/suggest/suggest-file-names.d.ts +2 -0
- package/dist/suggest/suggest-file-names.js +46 -0
- package/docs/architecture.md +250 -0
- package/docs/result-schema.md +222 -0
- package/package.json +51 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ReadMessageCatalog } from "../contract/messages.ts";
|
|
2
|
+
/** The default wording of every read message. */
|
|
3
|
+
export declare const defaultReadMessages: Readonly<ReadMessageCatalog>;
|
|
4
|
+
/**
|
|
5
|
+
* Merges overrides over `defaultReadMessages`. Throws TypeError on an unknown key
|
|
6
|
+
* or a value that is not a function.
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveReadMessages(overrides?: Partial<ReadMessageCatalog>): Readonly<ReadMessageCatalog>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const catalog = {
|
|
2
|
+
retry: (next) => JSON.stringify(next),
|
|
3
|
+
continuation: ({ retry, reason }) => `Output stopped at the ${reasonLabel(reason)}. Continue with ${retry}.`,
|
|
4
|
+
firstLineTooLong: ({ line, maxViewBytes, retry }) => `Line ${line} cannot be shown within the ${maxViewBytes}-byte view limit. To skip it, continue with ${retry}.`,
|
|
5
|
+
offsetPastEof: ({ request, totalLines, retry }) => `Line ${request.offset} is past EOF; the file has ${totalLines} logical line${totalLines === 1 ? "" : "s"}. To read the last line, retry with ${retry}.`,
|
|
6
|
+
offsetUnreached: ({ request, reachedLine, retry }) => `Scanning stopped before line ${request.offset} was reached. Retry from line ${reachedLine} with ${retry}.`,
|
|
7
|
+
lineClamped: ({ lines, total, maxChars }) => `Source line${total === 1 ? "" : "s"} ${lines.join(", ")}${total > lines.length ? ", …" : ""} exceeded ${maxChars} characters and ${total === 1 ? "was" : "were"} clamped.`,
|
|
8
|
+
limitClamped: ({ requested, max }) => `The requested line count of ${requested} is over the maximum, so at most ${max} lines are shown.`,
|
|
9
|
+
scanLimit: ({ maxScanBytes }) => `Scanning stopped at ${maxScanBytes} bytes, so the total line count is unknown.`,
|
|
10
|
+
empty: ({ path }) => `${path} is empty (0 bytes); no retry is needed.`,
|
|
11
|
+
notFound: ({ request, suggestions }) => suggestions.length === 0
|
|
12
|
+
? `${request.path} was not found.`
|
|
13
|
+
: `${request.path} was not found. Nearby names: ${quoted(suggestions)}.`,
|
|
14
|
+
pathRepaired: ({ from, to }) => `The requested filename ${JSON.stringify(from)} was repaired to the unique Unicode-equivalent path ${JSON.stringify(to)}.`,
|
|
15
|
+
notAFile: ({ request, kind }) => `${request.path} is a ${kindLabel(kind)}.`,
|
|
16
|
+
dangerousPath: ({ request, detail }) => `${request.path} is refused by policy${detail === null ? "" : ` (${detail})`}. Choose another path.`,
|
|
17
|
+
outsideAllowedRoots: ({ request }) => `${request.path} is outside every configured allowed root.`,
|
|
18
|
+
permissionDenied: ({ request }) => `Permission was denied for ${request.path}.`,
|
|
19
|
+
denied: ({ path, detail }) => `${path} was refused by policy${detail === null ? "" : ` (${detail})`}.`,
|
|
20
|
+
tooLarge: ({ request, stage, limit }) => {
|
|
21
|
+
if (stage === "backend") {
|
|
22
|
+
return `${request.path} is larger than the ${limit}-byte limit of this filesystem backend, so it was not read.`;
|
|
23
|
+
}
|
|
24
|
+
return stage === "convert"
|
|
25
|
+
? `${request.path} is larger than the ${limit}-byte conversion limit, so it was not converted.`
|
|
26
|
+
: `The media converted from ${request.path} is larger than the ${limit}-byte media limit, so it was not returned.`;
|
|
27
|
+
},
|
|
28
|
+
changedDuringRead: ({ request, retry }) => `${request.path} changed while it was being read. Read it again with ${retry} before relying on this result.`,
|
|
29
|
+
aborted: ({ phase }) => `The read was aborted during the ${phase} phase, so nothing was read.`,
|
|
30
|
+
invalidInput: ({ detail }) => `The read input was rejected: ${detail}`,
|
|
31
|
+
extensionFailed: ({ request, extension, phase }) => `The ${extension} extension failed during the ${phase} phase while reading ${request.path}; this is a host problem rather than a property of the file.`,
|
|
32
|
+
ioError: ({ request }) => `The backend failed while reading ${request === null ? "the requested path" : request.path}.`,
|
|
33
|
+
unsupportedBackend: ({ request, detail }) => `This host cannot read ${request.path}${detail === null ? "" : ` (${detail})`}. Do not retry.`,
|
|
34
|
+
viewModified: ({ hook }) => `The ${hook} hook changed this view, so it is not the exact file text.`,
|
|
35
|
+
};
|
|
36
|
+
/** The default wording of every read message. */
|
|
37
|
+
export const defaultReadMessages = Object.freeze(catalog);
|
|
38
|
+
function quoted(values) {
|
|
39
|
+
return values.map((value) => JSON.stringify(value)).join(", ");
|
|
40
|
+
}
|
|
41
|
+
function reasonLabel(reason) {
|
|
42
|
+
if (reason === "lines")
|
|
43
|
+
return "line limit";
|
|
44
|
+
if (reason === "bytes")
|
|
45
|
+
return "view-byte limit";
|
|
46
|
+
if (reason === "budget")
|
|
47
|
+
return "view budget";
|
|
48
|
+
if (reason === "scan-limit")
|
|
49
|
+
return "scan limit";
|
|
50
|
+
return "line-length limit";
|
|
51
|
+
}
|
|
52
|
+
function kindLabel(kind) {
|
|
53
|
+
if (kind === "fifo")
|
|
54
|
+
return "FIFO";
|
|
55
|
+
if (kind === "other")
|
|
56
|
+
return "non-regular file";
|
|
57
|
+
return kind;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Merges overrides over `defaultReadMessages`. Throws TypeError on an unknown key
|
|
61
|
+
* or a value that is not a function.
|
|
62
|
+
*/
|
|
63
|
+
export function resolveReadMessages(overrides = {}) {
|
|
64
|
+
if (overrides === null || typeof overrides !== "object" || Array.isArray(overrides)) {
|
|
65
|
+
throw new TypeError("messages must be an object");
|
|
66
|
+
}
|
|
67
|
+
const resolved = { ...defaultReadMessages };
|
|
68
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
69
|
+
if (!Object.hasOwn(defaultReadMessages, key))
|
|
70
|
+
throw new TypeError(`Unknown message: ${key}`);
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
continue;
|
|
73
|
+
if (typeof value !== "function")
|
|
74
|
+
throw new TypeError(`messages.${key} must be a function`);
|
|
75
|
+
resolved[key] = value;
|
|
76
|
+
}
|
|
77
|
+
return Object.freeze(resolved);
|
|
78
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
2
|
+
import type { ReadLimits } from "../contract/limits.ts";
|
|
3
|
+
import type { ReadMessageCatalog } from "../contract/messages.ts";
|
|
4
|
+
import type { ReadNote } from "../contract/result.ts";
|
|
5
|
+
import type { LineScanner } from "./scanner.ts";
|
|
6
|
+
export interface ViewNoteContext {
|
|
7
|
+
readonly scanner: LineScanner;
|
|
8
|
+
readonly request: ReadRequest;
|
|
9
|
+
readonly limits: Readonly<ReadLimits>;
|
|
10
|
+
readonly messages: Readonly<ReadMessageCatalog>;
|
|
11
|
+
readonly scanCapped: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildViewNotes(context: ViewNoteContext): ReadNote[];
|
|
14
|
+
export declare function emptyNote(messages: Readonly<ReadMessageCatalog>, path: string): ReadNote;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const MAX_LISTED_CLAMPED_LINES = 20;
|
|
2
|
+
export function buildViewNotes(context) {
|
|
3
|
+
const { scanner, request, limits, messages, scanCapped } = context;
|
|
4
|
+
const notes = [];
|
|
5
|
+
const offsetUnreached = scanCapped &&
|
|
6
|
+
scanner.lines.length === 0 &&
|
|
7
|
+
scanner.firstUnshown === null &&
|
|
8
|
+
request.offset > scanner.totalLines + 1;
|
|
9
|
+
if (offsetUnreached) {
|
|
10
|
+
const reachedLine = scanner.totalLines + 1;
|
|
11
|
+
const retry = retryFrom(request, reachedLine);
|
|
12
|
+
notes.push({
|
|
13
|
+
code: "offset-unreached",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
message: messages.offsetUnreached({ request, reachedLine, retry: messages.retry(retry) }),
|
|
16
|
+
retry,
|
|
17
|
+
data: { reachedLine },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
else if (scanner.selectionStop === "bytes" &&
|
|
21
|
+
scanner.lines.length === 0 &&
|
|
22
|
+
scanner.firstUnshown !== null) {
|
|
23
|
+
const line = scanner.firstUnshown;
|
|
24
|
+
const retry = retryFrom(request, line + 1);
|
|
25
|
+
notes.push({
|
|
26
|
+
code: "first-line-exceeds-byte-limit",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: messages.firstLineTooLong({
|
|
29
|
+
line,
|
|
30
|
+
maxViewBytes: limits.maxViewBytes,
|
|
31
|
+
retry: messages.retry(retry),
|
|
32
|
+
}),
|
|
33
|
+
retry,
|
|
34
|
+
data: { line },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
else if (scanner.firstUnshown !== null) {
|
|
38
|
+
const retry = retryFrom(request, scanner.firstUnshown);
|
|
39
|
+
const reason = scanner.selectionStop ?? "scan-limit";
|
|
40
|
+
notes.push({
|
|
41
|
+
code: "continue",
|
|
42
|
+
severity: "info",
|
|
43
|
+
message: messages.continuation({ request, retry: messages.retry(retry), reason }),
|
|
44
|
+
retry,
|
|
45
|
+
data: { reason },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const clamped = scanner.clampedLines;
|
|
49
|
+
if (clamped.length > 0) {
|
|
50
|
+
const listed = clamped.slice(0, MAX_LISTED_CLAMPED_LINES);
|
|
51
|
+
notes.push({
|
|
52
|
+
code: "line-clamped",
|
|
53
|
+
severity: "warning",
|
|
54
|
+
message: messages.lineClamped({
|
|
55
|
+
lines: listed,
|
|
56
|
+
total: clamped.length,
|
|
57
|
+
maxChars: limits.maxCharsPerLine,
|
|
58
|
+
}),
|
|
59
|
+
data: { lines: [...listed], total: clamped.length, maxChars: limits.maxCharsPerLine },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (scanCapped) {
|
|
63
|
+
notes.push({
|
|
64
|
+
code: "scan-limit",
|
|
65
|
+
severity: "warning",
|
|
66
|
+
message: messages.scanLimit({ maxScanBytes: limits.maxScanBytes }),
|
|
67
|
+
data: { maxScanBytes: limits.maxScanBytes },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (!scanCapped &&
|
|
71
|
+
scanner.lines.length === 0 &&
|
|
72
|
+
request.offset > scanner.totalLines &&
|
|
73
|
+
scanner.totalLines > 0) {
|
|
74
|
+
const retry = retryFrom(request, Math.max(1, scanner.totalLines));
|
|
75
|
+
notes.push({
|
|
76
|
+
code: "offset-past-eof",
|
|
77
|
+
severity: "info",
|
|
78
|
+
message: messages.offsetPastEof({
|
|
79
|
+
request,
|
|
80
|
+
totalLines: scanner.totalLines,
|
|
81
|
+
retry: messages.retry(retry),
|
|
82
|
+
}),
|
|
83
|
+
retry,
|
|
84
|
+
data: { totalLines: scanner.totalLines },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return notes;
|
|
88
|
+
}
|
|
89
|
+
export function emptyNote(messages, path) {
|
|
90
|
+
return { code: "empty", severity: "info", message: messages.empty({ path }) };
|
|
91
|
+
}
|
|
92
|
+
function retryFrom(request, offset) {
|
|
93
|
+
return { path: request.path, offset, limit: request.limit };
|
|
94
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Digest } from "../contract/digest.ts";
|
|
2
|
+
import type { ContentPart, FileInfo, ReadObservation } from "../contract/result.ts";
|
|
3
|
+
export interface ObservationInput {
|
|
4
|
+
readonly file: FileInfo;
|
|
5
|
+
readonly contentId: string | null;
|
|
6
|
+
readonly view: string | readonly ContentPart[];
|
|
7
|
+
readonly observedAt: string;
|
|
8
|
+
readonly wholeFileVisible: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function buildObservation(digest: Digest | null, input: ObservationInput): ReadObservation | null;
|
|
11
|
+
export declare function withEditedView(digest: Digest, observation: ReadObservation, view: string | readonly ContentPart[]): ReadObservation;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const ENCODER = new TextEncoder();
|
|
2
|
+
export function buildObservation(digest, input) {
|
|
3
|
+
if (digest === null)
|
|
4
|
+
return null;
|
|
5
|
+
const statId = digest.hash(JSON.stringify({
|
|
6
|
+
resolvedPath: input.file.resolvedPath,
|
|
7
|
+
identity: input.file.identity,
|
|
8
|
+
size: input.file.size,
|
|
9
|
+
mtimeMs: input.file.mtimeMs,
|
|
10
|
+
}));
|
|
11
|
+
const viewId = viewIdOf(digest, input.view);
|
|
12
|
+
return {
|
|
13
|
+
id: observationId(digest, statId, input.contentId, viewId),
|
|
14
|
+
statId,
|
|
15
|
+
contentId: input.contentId,
|
|
16
|
+
viewId,
|
|
17
|
+
observedAt: input.observedAt,
|
|
18
|
+
wholeFileVisible: input.wholeFileVisible,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function withEditedView(digest, observation, view) {
|
|
22
|
+
const viewId = viewIdOf(digest, view);
|
|
23
|
+
return {
|
|
24
|
+
...observation,
|
|
25
|
+
id: observationId(digest, observation.statId, observation.contentId, viewId),
|
|
26
|
+
viewId,
|
|
27
|
+
wholeFileVisible: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function viewIdOf(digest, view) {
|
|
31
|
+
return typeof view === "string" ? digest.hash(view) : partsId(digest, view);
|
|
32
|
+
}
|
|
33
|
+
function observationId(digest, statId, contentId, viewId) {
|
|
34
|
+
return digest.hash(JSON.stringify([statId, contentId, viewId]));
|
|
35
|
+
}
|
|
36
|
+
function partsId(digest, parts) {
|
|
37
|
+
const stream = digest.create();
|
|
38
|
+
// Type and length prefixes stop different part lists from hashing the same.
|
|
39
|
+
for (const part of parts) {
|
|
40
|
+
if (part.type === "text") {
|
|
41
|
+
const bytes = ENCODER.encode(part.text);
|
|
42
|
+
stream.update(ENCODER.encode(`text:${bytes.byteLength}:`));
|
|
43
|
+
stream.update(bytes);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
stream.update(ENCODER.encode(`media:${part.mediaType}:${part.data.byteLength}:`));
|
|
47
|
+
stream.update(part.data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return stream.digest();
|
|
51
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FileSystem, NotAFileError, OpenFile, OpenFileInfo } from "@better-fs-tools/fs";
|
|
2
|
+
import type { ReadDependencies } from "../contract/deps.ts";
|
|
3
|
+
import type { DirectoryConverter } from "../contract/extensions.ts";
|
|
4
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
5
|
+
import type { FileInfo } from "../contract/result.ts";
|
|
6
|
+
import type { CallScope } from "./call-scope.ts";
|
|
7
|
+
export type Opened<THost> = {
|
|
8
|
+
readonly kind: "file";
|
|
9
|
+
readonly handle: OpenFile;
|
|
10
|
+
} | {
|
|
11
|
+
readonly kind: "directory";
|
|
12
|
+
readonly error: NotAFileError;
|
|
13
|
+
readonly converter: DirectoryConverter<THost>;
|
|
14
|
+
};
|
|
15
|
+
export declare function openFile<THost>(deps: ReadDependencies<THost>, fs: FileSystem, request: ReadRequest, path: string, scope: CallScope<THost>): Promise<Opened<THost>>;
|
|
16
|
+
export declare function fileInfo(fs: FileSystem, request: ReadRequest, info: OpenFileInfo, resolvedFrom: string | null): FileInfo;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { directoryConverter } from "./directory.js";
|
|
2
|
+
import { ReadStop, fromFileSystemError } from "./outcomes.js";
|
|
3
|
+
import { missOutcome } from "./suggest.js";
|
|
4
|
+
export async function openFile(deps, fs, request, path, scope) {
|
|
5
|
+
const signal = scope.signal;
|
|
6
|
+
const opened = await fs.open(path, signal === undefined ? {} : { signal });
|
|
7
|
+
if (opened.ok)
|
|
8
|
+
return { kind: "file", handle: opened.file };
|
|
9
|
+
const { error } = opened;
|
|
10
|
+
if (error.reason === "not-found") {
|
|
11
|
+
throw new ReadStop(await missOutcome(deps, request, fs, scope, path, error));
|
|
12
|
+
}
|
|
13
|
+
if (error.reason === "not-a-file" && error.kind === "directory") {
|
|
14
|
+
const converter = directoryConverter(scope, fs);
|
|
15
|
+
if (converter !== null)
|
|
16
|
+
return { kind: "directory", error, converter };
|
|
17
|
+
}
|
|
18
|
+
throw new ReadStop(fromFileSystemError(deps.messages, request, error, scope.phase));
|
|
19
|
+
}
|
|
20
|
+
export function fileInfo(fs, request, info, resolvedFrom) {
|
|
21
|
+
return {
|
|
22
|
+
requestedPath: request.path,
|
|
23
|
+
resolvedPath: info.resolvedPath,
|
|
24
|
+
displayPath: info.displayPath,
|
|
25
|
+
backend: fs.id,
|
|
26
|
+
size: info.size,
|
|
27
|
+
mtimeMs: info.mtimeMs,
|
|
28
|
+
identity: fs.capabilities.identity ? info.identity : null,
|
|
29
|
+
mimeType: info.mimeType,
|
|
30
|
+
resolvedFrom,
|
|
31
|
+
version: info.version ?? null,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { FileSystemError } from "@better-fs-tools/fs";
|
|
2
|
+
import type { UnsupportedClassification } from "../contract/classify.ts";
|
|
3
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
4
|
+
import type { JsonValue } from "../contract/json.ts";
|
|
5
|
+
import type { ReadMessageCatalog, ReadPhase } from "../contract/messages.ts";
|
|
6
|
+
import type { FileInfo, ReadErrorCode, ReadFailure, ReadNote, ReadReport, ReadUnsupported } from "../contract/result.ts";
|
|
7
|
+
type Messages = Readonly<ReadMessageCatalog>;
|
|
8
|
+
export declare class ReadStop extends Error {
|
|
9
|
+
readonly outcome: ReadReport;
|
|
10
|
+
constructor(outcome: ReadReport);
|
|
11
|
+
}
|
|
12
|
+
export declare function failure(code: ReadErrorCode, phase: ReadPhase, request: ReadRequest | null, file: FileInfo | null, note: ReadNote): ReadFailure;
|
|
13
|
+
export declare function invalidInput(messages: Messages, input: unknown, error: unknown): ReadFailure;
|
|
14
|
+
export declare function aborted(messages: Messages, request: ReadRequest, phase: ReadPhase): ReadFailure;
|
|
15
|
+
export declare function extensionFailed(messages: Messages, request: ReadRequest, extension: string, phase: ReadPhase, id?: string | null): ReadFailure;
|
|
16
|
+
export declare function hostErrorNote(code: ReadErrorCode, note: ReadNote): ReadNote;
|
|
17
|
+
export declare function denied(messages: Messages, request: ReadRequest, note: ReadNote | null): ReadFailure;
|
|
18
|
+
export declare function ioError(messages: Messages, request: ReadRequest, phase: ReadPhase, error: unknown): ReadFailure;
|
|
19
|
+
export declare function changedDuringRead(messages: Messages, request: ReadRequest, file: FileInfo): ReadFailure;
|
|
20
|
+
export declare function unsupportedBackend(messages: Messages, request: ReadRequest, phase: ReadPhase, file: FileInfo, detail: string | null): ReadFailure;
|
|
21
|
+
export declare function unsupportedOutcome(request: ReadRequest, file: FileInfo, classifier: string, classification: UnsupportedClassification): ReadUnsupported;
|
|
22
|
+
export declare function fromFileSystemError(messages: Messages, request: ReadRequest, error: FileSystemError, phase: ReadPhase, file?: FileInfo | null): ReadFailure;
|
|
23
|
+
export declare function errorData(error: FileSystemError): Record<string, JsonValue>;
|
|
24
|
+
export declare function notFound(messages: Messages, request: ReadRequest, suggestions: readonly string[], data: Record<string, JsonValue>): ReadFailure;
|
|
25
|
+
export declare function isNote(value: unknown): value is ReadNote;
|
|
26
|
+
export declare function messageOf(error: unknown): string;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { classificationInfo } from "./classify.js";
|
|
2
|
+
import { isRecord } from "./input.js";
|
|
3
|
+
export class ReadStop extends Error {
|
|
4
|
+
outcome;
|
|
5
|
+
constructor(outcome) {
|
|
6
|
+
super(`read stopped with ${outcome.status}`);
|
|
7
|
+
this.outcome = outcome;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function failure(code, phase, request, file, note) {
|
|
11
|
+
const error = {
|
|
12
|
+
code,
|
|
13
|
+
phase,
|
|
14
|
+
message: note.message,
|
|
15
|
+
...(note.data === undefined ? {} : { data: note.data }),
|
|
16
|
+
};
|
|
17
|
+
return { tool: "read", status: "error", error, request, file, notes: [note] };
|
|
18
|
+
}
|
|
19
|
+
export function invalidInput(messages, input, error) {
|
|
20
|
+
const path = isRecord(input) && typeof input.path === "string" ? input.path : "";
|
|
21
|
+
return failure("INVALID_INPUT", "input", null, null, {
|
|
22
|
+
code: "invalid-input",
|
|
23
|
+
severity: "warning",
|
|
24
|
+
message: messages.invalidInput({ detail: messageOf(error) }),
|
|
25
|
+
...(path === "" ? {} : { data: { path } }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function aborted(messages, request, phase) {
|
|
29
|
+
return failure("ABORTED", phase, request, null, {
|
|
30
|
+
code: "aborted",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message: messages.aborted({ phase }),
|
|
33
|
+
data: { phase },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
export function extensionFailed(messages, request, extension, phase, id = null) {
|
|
37
|
+
return failure("EXTENSION_FAILED", phase, request, null, {
|
|
38
|
+
code: "extension-failed",
|
|
39
|
+
severity: "warning",
|
|
40
|
+
message: messages.extensionFailed({ request, extension, phase }),
|
|
41
|
+
data: id === null ? { extension, phase } : { extension, phase, id },
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function hostErrorNote(code, note) {
|
|
45
|
+
const kebab = code.toLowerCase().replaceAll("_", "-");
|
|
46
|
+
const data = note.code === kebab ? note.data : { ...note.data, source: note.code };
|
|
47
|
+
return {
|
|
48
|
+
code: kebab,
|
|
49
|
+
severity: "warning",
|
|
50
|
+
message: note.message,
|
|
51
|
+
...(data === undefined ? {} : { data }),
|
|
52
|
+
...(note.retry === undefined ? {} : { retry: note.retry }),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function denied(messages, request, note) {
|
|
56
|
+
return failure("DENIED", "authorize", request,
|
|
57
|
+
// No file info: a denial must not disclose the open target.
|
|
58
|
+
null, note === null
|
|
59
|
+
? {
|
|
60
|
+
code: "denied",
|
|
61
|
+
severity: "warning",
|
|
62
|
+
message: messages.denied({ path: request.path, detail: null }),
|
|
63
|
+
}
|
|
64
|
+
: hostErrorNote("DENIED", note));
|
|
65
|
+
}
|
|
66
|
+
export function ioError(messages, request, phase, error) {
|
|
67
|
+
return failure("IO_ERROR", phase, request, null, {
|
|
68
|
+
code: "io-error",
|
|
69
|
+
severity: "warning",
|
|
70
|
+
message: messages.ioError({ request }),
|
|
71
|
+
data: { detail: messageOf(error) },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
export function changedDuringRead(messages, request, file) {
|
|
75
|
+
const retry = { path: request.path, offset: request.offset, limit: request.limit };
|
|
76
|
+
return failure("CHANGED_DURING_READ", "verification", request, file, {
|
|
77
|
+
code: "changed-during-read",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
message: messages.changedDuringRead({ request, retry: messages.retry(retry) }),
|
|
80
|
+
retry,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export function unsupportedBackend(messages, request, phase, file, detail) {
|
|
84
|
+
return failure("UNSUPPORTED_BACKEND", phase, request, file, {
|
|
85
|
+
code: "unsupported-backend",
|
|
86
|
+
severity: "warning",
|
|
87
|
+
message: messages.unsupportedBackend({ request, detail }),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export function unsupportedOutcome(request, file, classifier, classification) {
|
|
91
|
+
return {
|
|
92
|
+
tool: "read",
|
|
93
|
+
status: "unsupported",
|
|
94
|
+
code: classification.code,
|
|
95
|
+
request,
|
|
96
|
+
file,
|
|
97
|
+
classification: classificationInfo({ classifier, classification }),
|
|
98
|
+
notes: [classification.note],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const CODE_BY_REASON = {
|
|
102
|
+
"not-found": "NOT_FOUND",
|
|
103
|
+
"not-a-file": "NOT_A_FILE",
|
|
104
|
+
"dangerous-path": "DANGEROUS_PATH",
|
|
105
|
+
"outside-allowed-roots": "OUTSIDE_ALLOWED_ROOTS",
|
|
106
|
+
"permission-denied": "PERMISSION_DENIED",
|
|
107
|
+
denied: "DENIED",
|
|
108
|
+
"too-large": "TOO_LARGE",
|
|
109
|
+
unsupported: "UNSUPPORTED_BACKEND",
|
|
110
|
+
aborted: "ABORTED",
|
|
111
|
+
io: "IO_ERROR",
|
|
112
|
+
};
|
|
113
|
+
export function fromFileSystemError(messages, request, error, phase, file = null) {
|
|
114
|
+
const code = CODE_BY_REASON[error.reason];
|
|
115
|
+
if (code === "ABORTED")
|
|
116
|
+
return aborted(messages, request, phase);
|
|
117
|
+
const detail = error.detail ?? null;
|
|
118
|
+
const data = errorData(error);
|
|
119
|
+
return failure(code, phase, request, file, {
|
|
120
|
+
code: code.toLowerCase().replaceAll("_", "-"),
|
|
121
|
+
severity: "warning",
|
|
122
|
+
message: messageForError(messages, request, error, detail),
|
|
123
|
+
...(Object.keys(data).length === 0 ? {} : { data }),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export function errorData(error) {
|
|
127
|
+
return {
|
|
128
|
+
...(error.reason === "not-a-file" ? { kind: error.kind } : {}),
|
|
129
|
+
...(error.reason === "too-large" ? { limit: error.limit, size: error.size } : {}),
|
|
130
|
+
...(error.detail === undefined ? {} : { detail: error.detail }),
|
|
131
|
+
...(error.cause === undefined ? {} : { cause: { ...error.cause } }),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function notFound(messages, request, suggestions, data) {
|
|
135
|
+
return failure("NOT_FOUND", "resolve", request, null, {
|
|
136
|
+
code: "not-found",
|
|
137
|
+
severity: "warning",
|
|
138
|
+
message: messages.notFound({ request, suggestions }),
|
|
139
|
+
...(Object.keys(data).length === 0 ? {} : { data }),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function messageForError(messages, request, error, detail) {
|
|
143
|
+
switch (error.reason) {
|
|
144
|
+
case "not-found":
|
|
145
|
+
return messages.notFound({ request, suggestions: [] });
|
|
146
|
+
case "not-a-file":
|
|
147
|
+
return messages.notAFile({ request, kind: error.kind });
|
|
148
|
+
case "dangerous-path":
|
|
149
|
+
return messages.dangerousPath({ request, detail });
|
|
150
|
+
case "outside-allowed-roots":
|
|
151
|
+
return messages.outsideAllowedRoots({ request, detail });
|
|
152
|
+
case "permission-denied":
|
|
153
|
+
return messages.permissionDenied({ request, detail });
|
|
154
|
+
case "denied":
|
|
155
|
+
return messages.denied({ path: request.path, detail });
|
|
156
|
+
case "too-large":
|
|
157
|
+
return messages.tooLarge({ request, stage: "backend", limit: error.limit });
|
|
158
|
+
case "unsupported":
|
|
159
|
+
return messages.unsupportedBackend({ request, detail });
|
|
160
|
+
default:
|
|
161
|
+
return messages.ioError({ request });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export function isNote(value) {
|
|
165
|
+
return (isRecord(value) &&
|
|
166
|
+
typeof value.code === "string" &&
|
|
167
|
+
typeof value.message === "string" &&
|
|
168
|
+
(value.severity === "info" || value.severity === "warning"));
|
|
169
|
+
}
|
|
170
|
+
export function messageOf(error) {
|
|
171
|
+
return error instanceof Error ? error.message : String(error);
|
|
172
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ReadContext } from "../contract/context.ts";
|
|
2
|
+
import type { ReadDependencies } from "../contract/deps.ts";
|
|
3
|
+
import type { ReadResult } from "../contract/result.ts";
|
|
4
|
+
export declare function runRead<THost>(deps: ReadDependencies<THost>, input: unknown, call: ReadContext<THost>): Promise<ReadResult>;
|