@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,112 @@
|
|
|
1
|
+
import { authorizeList } from "./authorize.js";
|
|
2
|
+
import { AbortReadError } from "./cursor.js";
|
|
3
|
+
import { isFileSystem, isStateStore } from "./deps.js";
|
|
4
|
+
import { ReadStop, extensionFailed, messageOf } from "./outcomes.js";
|
|
5
|
+
export class CallScope {
|
|
6
|
+
deps;
|
|
7
|
+
request;
|
|
8
|
+
call;
|
|
9
|
+
phase = "input";
|
|
10
|
+
allowNotes = [];
|
|
11
|
+
heldFailure = null;
|
|
12
|
+
resolvedFs = null;
|
|
13
|
+
resolvedState = undefined;
|
|
14
|
+
hook = null;
|
|
15
|
+
listed = new Set();
|
|
16
|
+
constructor(deps, request, call) {
|
|
17
|
+
this.deps = deps;
|
|
18
|
+
this.request = request;
|
|
19
|
+
this.call = call;
|
|
20
|
+
}
|
|
21
|
+
get signal() {
|
|
22
|
+
return this.call.signal;
|
|
23
|
+
}
|
|
24
|
+
enter(phase) {
|
|
25
|
+
this.phase = phase;
|
|
26
|
+
}
|
|
27
|
+
checkAbort() {
|
|
28
|
+
if (this.call.signal?.aborted)
|
|
29
|
+
throw new AbortReadError("aborted");
|
|
30
|
+
}
|
|
31
|
+
fileSystem() {
|
|
32
|
+
if (this.resolvedFs !== null)
|
|
33
|
+
return this.resolvedFs;
|
|
34
|
+
const { fs } = this.deps;
|
|
35
|
+
if (typeof fs !== "function")
|
|
36
|
+
return (this.resolvedFs = fs);
|
|
37
|
+
let produced;
|
|
38
|
+
try {
|
|
39
|
+
produced = fs(this.call);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw this.extensionFailure("fs");
|
|
43
|
+
}
|
|
44
|
+
if (!isFileSystem(produced))
|
|
45
|
+
throw this.extensionFailure("fs");
|
|
46
|
+
return (this.resolvedFs = produced);
|
|
47
|
+
}
|
|
48
|
+
stateStore() {
|
|
49
|
+
if (this.resolvedState !== undefined)
|
|
50
|
+
return this.resolvedState;
|
|
51
|
+
const { state } = this.deps;
|
|
52
|
+
if (typeof state !== "function")
|
|
53
|
+
return (this.resolvedState = state);
|
|
54
|
+
let produced;
|
|
55
|
+
try {
|
|
56
|
+
produced = state(this.call);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
throw this.extensionFailure("state");
|
|
60
|
+
}
|
|
61
|
+
if (produced !== null && !isStateStore(produced))
|
|
62
|
+
throw this.extensionFailure("state");
|
|
63
|
+
return (this.resolvedState = produced);
|
|
64
|
+
}
|
|
65
|
+
hookContext() {
|
|
66
|
+
if (this.hook !== null)
|
|
67
|
+
return this.hook;
|
|
68
|
+
const { limits, messages, digest, clock } = this.deps;
|
|
69
|
+
return (this.hook = Object.freeze({
|
|
70
|
+
tool: "read",
|
|
71
|
+
request: this.request,
|
|
72
|
+
limits,
|
|
73
|
+
messages,
|
|
74
|
+
digest,
|
|
75
|
+
clock,
|
|
76
|
+
call: this.call,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/** One bounded, authorized listing per slot. Never throws: every failure is an error outcome. */
|
|
80
|
+
async list(slot, dir, displayPath = dir) {
|
|
81
|
+
if (this.listed.has(slot)) {
|
|
82
|
+
return { ok: false, error: { reason: "denied", detail: "listing budget spent" } };
|
|
83
|
+
}
|
|
84
|
+
this.listed.add(slot);
|
|
85
|
+
const fs = this.fileSystem();
|
|
86
|
+
if (typeof fs.list !== "function") {
|
|
87
|
+
return { ok: false, error: { reason: "unsupported", detail: "the backend cannot list" } };
|
|
88
|
+
}
|
|
89
|
+
const refused = await authorizeList(this.deps.authorize, this.request, dir, displayPath, this);
|
|
90
|
+
if (refused !== null)
|
|
91
|
+
return refused;
|
|
92
|
+
const signal = this.signal;
|
|
93
|
+
const limit = this.deps.limits.maxDirectoryEntries;
|
|
94
|
+
try {
|
|
95
|
+
return await fs.list(dir, signal === undefined ? { limit } : { limit, signal });
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
return { ok: false, error: { reason: "io", detail: messageOf(error) } };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Keeps a listing failure out of reach of host code that might catch and ignore it. */
|
|
102
|
+
hold(stop) {
|
|
103
|
+
this.heldFailure ??= stop;
|
|
104
|
+
}
|
|
105
|
+
throwHeld() {
|
|
106
|
+
if (this.heldFailure !== null)
|
|
107
|
+
throw this.heldFailure;
|
|
108
|
+
}
|
|
109
|
+
extensionFailure(extension, id = null) {
|
|
110
|
+
return new ReadStop(extensionFailed(this.deps.messages, this.request, extension, this.phase, id));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Classification, ClassificationSample, Classifier, UnsupportedClassification } from "../contract/classify.ts";
|
|
2
|
+
import type { ClassificationInfo } from "../contract/result.ts";
|
|
3
|
+
export interface Decision<T> {
|
|
4
|
+
readonly classifier: string;
|
|
5
|
+
readonly classification: T;
|
|
6
|
+
}
|
|
7
|
+
export declare function classifySample(classifiers: readonly Classifier[], sample: ClassificationSample): Decision<Classification> | null;
|
|
8
|
+
export declare function encodingRefusal(classifiers: readonly Classifier[], sample: ClassificationSample): Decision<UnsupportedClassification> | null;
|
|
9
|
+
export declare function classificationInfo(decision: Decision<Classification>): ClassificationInfo;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export function classifySample(classifiers, sample) {
|
|
2
|
+
for (const candidate of classifiers) {
|
|
3
|
+
const classification = candidate.classify(sample);
|
|
4
|
+
if (classification !== null)
|
|
5
|
+
return { classifier: candidate.id, classification };
|
|
6
|
+
}
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
export function encodingRefusal(classifiers, sample) {
|
|
10
|
+
for (const candidate of classifiers) {
|
|
11
|
+
const classification = candidate.encodingFailure?.(sample) ?? null;
|
|
12
|
+
if (classification !== null)
|
|
13
|
+
return { classifier: candidate.id, classification };
|
|
14
|
+
}
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
export function classificationInfo(decision) {
|
|
18
|
+
const { classifier, classification } = decision;
|
|
19
|
+
return {
|
|
20
|
+
kind: classification.kind,
|
|
21
|
+
classifier,
|
|
22
|
+
code: classification.kind === "unsupported" ? classification.code : null,
|
|
23
|
+
mimeType: classification.mimeType,
|
|
24
|
+
confidence: classification.confidence,
|
|
25
|
+
reasons: classification.reasons,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { ClassificationSample } from "../contract/classify.ts";
|
|
2
|
+
import type { Digest } from "../contract/digest.ts";
|
|
3
|
+
import type { CallScope } from "./call-scope.ts";
|
|
4
|
+
import type { ByteCursor } from "./cursor.ts";
|
|
5
|
+
export declare class ConvertLimitError extends Error {
|
|
6
|
+
}
|
|
7
|
+
export declare class ConvertSource<THost> {
|
|
8
|
+
private readonly cursor;
|
|
9
|
+
private readonly sample;
|
|
10
|
+
private readonly limit;
|
|
11
|
+
private readonly scope;
|
|
12
|
+
bytesRead: number;
|
|
13
|
+
/** Stays set even when the converter catches the ConvertLimitError. */
|
|
14
|
+
exceeded: boolean;
|
|
15
|
+
reachedEof: boolean;
|
|
16
|
+
/** Stays set even when the converter catches the backend error. */
|
|
17
|
+
backendFailure: unknown;
|
|
18
|
+
private readonly hash;
|
|
19
|
+
private cachedContentId;
|
|
20
|
+
private sampleSent;
|
|
21
|
+
private started;
|
|
22
|
+
constructor(cursor: ByteCursor, sample: ClassificationSample, limit: number, digest: Digest | null, scope: CallScope<THost>);
|
|
23
|
+
bytes(): AsyncIterable<Uint8Array>;
|
|
24
|
+
drain(): Promise<void>;
|
|
25
|
+
contentId(): string | null;
|
|
26
|
+
private iterate;
|
|
27
|
+
private pull;
|
|
28
|
+
private nextChunk;
|
|
29
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AbortReadError } from "./cursor.js";
|
|
2
|
+
export class ConvertLimitError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export class ConvertSource {
|
|
5
|
+
cursor;
|
|
6
|
+
sample;
|
|
7
|
+
limit;
|
|
8
|
+
scope;
|
|
9
|
+
bytesRead = 0;
|
|
10
|
+
/** Stays set even when the converter catches the ConvertLimitError. */
|
|
11
|
+
exceeded = false;
|
|
12
|
+
reachedEof = false;
|
|
13
|
+
/** Stays set even when the converter catches the backend error. */
|
|
14
|
+
backendFailure = null;
|
|
15
|
+
hash;
|
|
16
|
+
cachedContentId;
|
|
17
|
+
sampleSent = false;
|
|
18
|
+
started = false;
|
|
19
|
+
constructor(cursor, sample, limit, digest, scope) {
|
|
20
|
+
this.cursor = cursor;
|
|
21
|
+
this.sample = sample;
|
|
22
|
+
this.limit = limit;
|
|
23
|
+
this.scope = scope;
|
|
24
|
+
this.hash = digest === null ? null : digest.create();
|
|
25
|
+
}
|
|
26
|
+
bytes() {
|
|
27
|
+
if (this.started)
|
|
28
|
+
throw new TypeError("the converter source is single use");
|
|
29
|
+
this.started = true;
|
|
30
|
+
return this.iterate();
|
|
31
|
+
}
|
|
32
|
+
async drain() {
|
|
33
|
+
this.started = true;
|
|
34
|
+
while ((await this.pull()) !== null) { }
|
|
35
|
+
}
|
|
36
|
+
contentId() {
|
|
37
|
+
if (!this.reachedEof)
|
|
38
|
+
return null;
|
|
39
|
+
this.cachedContentId ??= this.hash?.digest() ?? null;
|
|
40
|
+
return this.cachedContentId;
|
|
41
|
+
}
|
|
42
|
+
async *iterate() {
|
|
43
|
+
for (;;) {
|
|
44
|
+
const chunk = await this.pull();
|
|
45
|
+
if (chunk === null)
|
|
46
|
+
return;
|
|
47
|
+
yield chunk;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async pull() {
|
|
51
|
+
if (this.exceeded)
|
|
52
|
+
throw new ConvertLimitError("conversion limit exceeded");
|
|
53
|
+
if (this.reachedEof)
|
|
54
|
+
return null;
|
|
55
|
+
this.scope.checkAbort();
|
|
56
|
+
const chunk = await this.nextChunk();
|
|
57
|
+
if (chunk === null) {
|
|
58
|
+
this.reachedEof = true;
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
if (this.bytesRead + chunk.byteLength > this.limit) {
|
|
62
|
+
this.exceeded = true;
|
|
63
|
+
throw new ConvertLimitError("conversion limit exceeded");
|
|
64
|
+
}
|
|
65
|
+
this.bytesRead += chunk.byteLength;
|
|
66
|
+
this.hash?.update(chunk);
|
|
67
|
+
return chunk;
|
|
68
|
+
}
|
|
69
|
+
async nextChunk() {
|
|
70
|
+
if (!this.sampleSent) {
|
|
71
|
+
this.sampleSent = true;
|
|
72
|
+
if (this.sample.bytes.byteLength > 0)
|
|
73
|
+
return this.sample.bytes;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const item = await this.cursor.next(this.scope.signal);
|
|
77
|
+
return item.done ? null : item.value;
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (!(error instanceof AbortReadError))
|
|
81
|
+
this.backendFailure ??= error;
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FileSystem, OpenFile } from "@better-fs-tools/fs";
|
|
2
|
+
import type { Classification, ClassificationSample } from "../contract/classify.ts";
|
|
3
|
+
import type { ReadDependencies } from "../contract/deps.ts";
|
|
4
|
+
import type { FileConverter } from "../contract/extensions.ts";
|
|
5
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
6
|
+
import type { FileInfo, ReadReport } from "../contract/result.ts";
|
|
7
|
+
import type { CallScope } from "./call-scope.ts";
|
|
8
|
+
import type { Decision } from "./classify.ts";
|
|
9
|
+
import type { ByteCursor } from "./cursor.ts";
|
|
10
|
+
export declare function selectFileConverter<THost>(scope: CallScope<THost>, decision: Decision<Classification>, sample: ClassificationSample): FileConverter<THost> | null;
|
|
11
|
+
export interface ConvertFileInput<THost> {
|
|
12
|
+
readonly deps: ReadDependencies<THost>;
|
|
13
|
+
readonly fs: FileSystem;
|
|
14
|
+
readonly request: ReadRequest;
|
|
15
|
+
readonly file: FileInfo;
|
|
16
|
+
readonly handle: OpenFile;
|
|
17
|
+
readonly cursor: ByteCursor;
|
|
18
|
+
readonly sample: ClassificationSample;
|
|
19
|
+
readonly decision: Decision<Classification>;
|
|
20
|
+
readonly converter: FileConverter<THost>;
|
|
21
|
+
readonly scope: CallScope<THost>;
|
|
22
|
+
}
|
|
23
|
+
export declare function convertFile<THost>(input: ConvertFileInput<THost>): Promise<ReadReport>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { classificationInfo } from "./classify.js";
|
|
2
|
+
import { ConvertSource } from "./convert-source.js";
|
|
3
|
+
import { checkConvertOutcome, firstMediaType, mediaBytes, refusedOutcome, scanConvertedText, tooLarge, } from "./converted.js";
|
|
4
|
+
import { AbortReadError, raceAbort } from "./cursor.js";
|
|
5
|
+
import { extensionId } from "./extension-error.js";
|
|
6
|
+
import { buildObservation } from "./observation.js";
|
|
7
|
+
import { ReadStop } from "./outcomes.js";
|
|
8
|
+
import { textOutcome } from "./text-outcome.js";
|
|
9
|
+
import { checkSize, verifyHandle } from "./verify.js";
|
|
10
|
+
export function selectFileConverter(scope, decision, sample) {
|
|
11
|
+
const match = Object.freeze({
|
|
12
|
+
classification: decision.classification,
|
|
13
|
+
classifier: decision.classifier,
|
|
14
|
+
sample,
|
|
15
|
+
});
|
|
16
|
+
for (const converter of scope.deps.converters) {
|
|
17
|
+
if (converter.target !== "file")
|
|
18
|
+
continue;
|
|
19
|
+
scope.enter("conversion");
|
|
20
|
+
let accepted;
|
|
21
|
+
try {
|
|
22
|
+
accepted = converter.accepts(match);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw scope.extensionFailure("converters", extensionId(converter, error));
|
|
26
|
+
}
|
|
27
|
+
if (accepted === true)
|
|
28
|
+
return converter;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export async function convertFile(input) {
|
|
33
|
+
const { deps, request, file, handle, sample, decision, converter, scope } = input;
|
|
34
|
+
const { limits, messages } = deps;
|
|
35
|
+
const classification = classificationInfo(decision);
|
|
36
|
+
const source = new ConvertSource(input.cursor, sample, limits.maxConvertBytes, deps.digest, scope);
|
|
37
|
+
const throwIfSourceFailed = () => {
|
|
38
|
+
scope.checkAbort();
|
|
39
|
+
if (source.exceeded) {
|
|
40
|
+
throw new ReadStop(tooLarge(messages, request, file, classification, "convert", limits.maxConvertBytes));
|
|
41
|
+
}
|
|
42
|
+
if (source.backendFailure !== null)
|
|
43
|
+
throw source.backendFailure;
|
|
44
|
+
};
|
|
45
|
+
const failed = (error) => {
|
|
46
|
+
throwIfSourceFailed();
|
|
47
|
+
return scope.extensionFailure("converters", extensionId(converter, error));
|
|
48
|
+
};
|
|
49
|
+
scope.enter("conversion");
|
|
50
|
+
const convertInput = Object.freeze({
|
|
51
|
+
info: handle.info,
|
|
52
|
+
classification: decision.classification,
|
|
53
|
+
sample,
|
|
54
|
+
bytes: () => source.bytes(),
|
|
55
|
+
});
|
|
56
|
+
let produced;
|
|
57
|
+
try {
|
|
58
|
+
produced = await raceAbort(() => converter.convert(convertInput, scope.hookContext()), scope.signal);
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw failed(error);
|
|
62
|
+
}
|
|
63
|
+
throwIfSourceFailed();
|
|
64
|
+
const outcome = checkConvertOutcome(produced);
|
|
65
|
+
if (outcome === null)
|
|
66
|
+
throw failed();
|
|
67
|
+
if (outcome.kind === "refuse") {
|
|
68
|
+
return refusedOutcome(request, file, classification, outcome.code, outcome.note);
|
|
69
|
+
}
|
|
70
|
+
if (outcome.kind === "media") {
|
|
71
|
+
if (mediaBytes(outcome.parts) > limits.maxMediaBytes) {
|
|
72
|
+
return tooLarge(messages, request, file, classification, "media", limits.maxMediaBytes);
|
|
73
|
+
}
|
|
74
|
+
await drainAndVerify(source, handle, input, throwIfSourceFailed);
|
|
75
|
+
return mediaOutcome(input, classification, outcome.parts, outcome.notes ?? [], source);
|
|
76
|
+
}
|
|
77
|
+
let scan;
|
|
78
|
+
try {
|
|
79
|
+
scan = await scanConvertedText(outcome.text, request, file, scope);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
if (error instanceof ReadStop || error instanceof AbortReadError)
|
|
83
|
+
throw error;
|
|
84
|
+
throw failed(error);
|
|
85
|
+
}
|
|
86
|
+
await drainAndVerify(source, handle, input, throwIfSourceFailed);
|
|
87
|
+
return textOutcome({
|
|
88
|
+
deps,
|
|
89
|
+
fs: input.fs,
|
|
90
|
+
request,
|
|
91
|
+
file,
|
|
92
|
+
classification,
|
|
93
|
+
conversion: { converter: converter.id, mimeType: outcome.mimeType },
|
|
94
|
+
scan,
|
|
95
|
+
contentId: source.contentId(),
|
|
96
|
+
notes: outcome.notes ?? [],
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
async function drainAndVerify(source, handle, input, throwIfSourceFailed) {
|
|
100
|
+
const { deps, request, file, scope } = input;
|
|
101
|
+
try {
|
|
102
|
+
await source.drain();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
throwIfSourceFailed();
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
scope.enter("verification");
|
|
109
|
+
checkSize(deps.messages, request, file, source.bytesRead);
|
|
110
|
+
await verifyHandle(handle, deps.messages, request, file, scope);
|
|
111
|
+
}
|
|
112
|
+
function mediaOutcome(input, classification, parts, notes, source) {
|
|
113
|
+
const { deps, request, file, converter } = input;
|
|
114
|
+
return {
|
|
115
|
+
tool: "read",
|
|
116
|
+
status: "media",
|
|
117
|
+
request,
|
|
118
|
+
file,
|
|
119
|
+
classification,
|
|
120
|
+
conversion: { converter: converter.id, mimeType: firstMediaType(parts) },
|
|
121
|
+
parts,
|
|
122
|
+
observation: buildObservation(deps.digest, {
|
|
123
|
+
file,
|
|
124
|
+
contentId: source.contentId(),
|
|
125
|
+
view: parts,
|
|
126
|
+
observedAt: deps.clock().toISOString(),
|
|
127
|
+
// Media is not source text, so it cannot back a text edit.
|
|
128
|
+
wholeFileVisible: false,
|
|
129
|
+
}),
|
|
130
|
+
notes,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConvertOutcome } from "../contract/extensions.ts";
|
|
2
|
+
import type { ReadRequest } from "../contract/input.ts";
|
|
3
|
+
import type { ReadMessageCatalog } from "../contract/messages.ts";
|
|
4
|
+
import type { ClassificationInfo, ContentPart, FileInfo, ReadNote, ReadUnsupported } from "../contract/result.ts";
|
|
5
|
+
import type { CallScope } from "./call-scope.ts";
|
|
6
|
+
import type { ScanOutcome } from "./scan.ts";
|
|
7
|
+
export declare class ConverterOutputError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export type ScannedText = Extract<ScanOutcome, {
|
|
10
|
+
kind: "scanned";
|
|
11
|
+
}>;
|
|
12
|
+
export declare function checkConvertOutcome(value: unknown): ConvertOutcome | null;
|
|
13
|
+
export declare function scanConvertedText<THost>(text: string | AsyncIterable<string>, request: ReadRequest, file: FileInfo, scope: CallScope<THost>): Promise<ScannedText>;
|
|
14
|
+
export declare function mediaBytes(parts: readonly ContentPart[]): number;
|
|
15
|
+
export declare function firstMediaType(parts: readonly ContentPart[]): string | null;
|
|
16
|
+
export declare function refusedOutcome(request: ReadRequest, file: FileInfo, classification: ClassificationInfo, code: string, note: ReadNote): ReadUnsupported;
|
|
17
|
+
export declare function tooLarge(messages: Readonly<ReadMessageCatalog>, request: ReadRequest, file: FileInfo, classification: ClassificationInfo, stage: "convert" | "media", limit: number): ReadUnsupported;
|
|
18
|
+
export declare function isContentPart(value: unknown): value is ContentPart;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ByteCursor } from "./cursor.js";
|
|
2
|
+
import { isRecord } from "./input.js";
|
|
3
|
+
import { isNote } from "./outcomes.js";
|
|
4
|
+
import { scanText } from "./scan.js";
|
|
5
|
+
const ENCODER = new TextEncoder();
|
|
6
|
+
export class ConverterOutputError extends Error {
|
|
7
|
+
}
|
|
8
|
+
export function checkConvertOutcome(value) {
|
|
9
|
+
if (!isRecord(value))
|
|
10
|
+
return null;
|
|
11
|
+
switch (value.kind) {
|
|
12
|
+
case "text": {
|
|
13
|
+
const { text, mimeType } = value;
|
|
14
|
+
const textOk = typeof text === "string" || isAsyncIterable(text);
|
|
15
|
+
const mimeOk = mimeType === null || typeof mimeType === "string";
|
|
16
|
+
return textOk && mimeOk && notesOk(value.notes) ? value : null;
|
|
17
|
+
}
|
|
18
|
+
case "media": {
|
|
19
|
+
const { parts } = value;
|
|
20
|
+
const partsOk = Array.isArray(parts) && parts.every(isContentPart);
|
|
21
|
+
return partsOk && notesOk(value.notes) ? value : null;
|
|
22
|
+
}
|
|
23
|
+
case "refuse":
|
|
24
|
+
return typeof value.code === "string" && value.code !== "" && isNote(value.note)
|
|
25
|
+
? value
|
|
26
|
+
: null;
|
|
27
|
+
default:
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function scanConvertedText(text, request, file, scope) {
|
|
32
|
+
const cursor = new ByteCursor(encoded(text));
|
|
33
|
+
try {
|
|
34
|
+
const scan = await scanText({
|
|
35
|
+
cursor,
|
|
36
|
+
sample: { bytes: new Uint8Array(0), complete: false, path: file.displayPath, mimeType: null },
|
|
37
|
+
request,
|
|
38
|
+
limits: scope.deps.limits,
|
|
39
|
+
digest: null,
|
|
40
|
+
scope,
|
|
41
|
+
});
|
|
42
|
+
// Encoded JS strings are valid UTF-8.
|
|
43
|
+
if (scan.kind !== "scanned")
|
|
44
|
+
throw new ConverterOutputError("converted text is not UTF-8");
|
|
45
|
+
return scan;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
await cursor.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function mediaBytes(parts) {
|
|
52
|
+
let total = 0;
|
|
53
|
+
for (const part of parts)
|
|
54
|
+
if (part.type === "media")
|
|
55
|
+
total += part.data.byteLength;
|
|
56
|
+
return total;
|
|
57
|
+
}
|
|
58
|
+
export function firstMediaType(parts) {
|
|
59
|
+
for (const part of parts)
|
|
60
|
+
if (part.type === "media")
|
|
61
|
+
return part.mediaType;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
export function refusedOutcome(request, file, classification, code, note) {
|
|
65
|
+
return {
|
|
66
|
+
tool: "read",
|
|
67
|
+
status: "unsupported",
|
|
68
|
+
code,
|
|
69
|
+
request,
|
|
70
|
+
file,
|
|
71
|
+
classification,
|
|
72
|
+
notes: [note],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function tooLarge(messages, request, file, classification, stage, limit) {
|
|
76
|
+
return refusedOutcome(request, file, classification, "TOO_LARGE", {
|
|
77
|
+
code: "too-large",
|
|
78
|
+
severity: "warning",
|
|
79
|
+
message: messages.tooLarge({ request, stage, limit }),
|
|
80
|
+
data: { stage, limit },
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function* encoded(text) {
|
|
84
|
+
if (typeof text === "string") {
|
|
85
|
+
yield ENCODER.encode(text);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
for await (const chunk of text) {
|
|
89
|
+
if (typeof chunk !== "string")
|
|
90
|
+
throw new ConverterOutputError("text chunks must be strings");
|
|
91
|
+
yield ENCODER.encode(chunk);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function isAsyncIterable(value) {
|
|
95
|
+
return (value !== null &&
|
|
96
|
+
typeof value === "object" &&
|
|
97
|
+
typeof value[Symbol.asyncIterator] === "function");
|
|
98
|
+
}
|
|
99
|
+
function notesOk(notes) {
|
|
100
|
+
return notes === undefined || (Array.isArray(notes) && notes.every(isNote));
|
|
101
|
+
}
|
|
102
|
+
export function isContentPart(value) {
|
|
103
|
+
if (!isRecord(value))
|
|
104
|
+
return false;
|
|
105
|
+
if (value.type === "text")
|
|
106
|
+
return typeof value.text === "string";
|
|
107
|
+
return (value.type === "media" &&
|
|
108
|
+
typeof value.mediaType === "string" &&
|
|
109
|
+
value.data instanceof Uint8Array &&
|
|
110
|
+
(value.name === undefined || typeof value.name === "string"));
|
|
111
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReadTool } from "../contract/context.ts";
|
|
2
|
+
import type { ReadToolDeps, StateNeedsDigest } from "../contract/deps.ts";
|
|
3
|
+
/**
|
|
4
|
+
* Creates the read tool. Throws TypeError on an unknown or malformed dependency,
|
|
5
|
+
* a missing `fs`, an empty `classifiers` list, or a `state` without a `digest`.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createReadTool<THost = undefined>(deps: ReadToolDeps<THost> & StateNeedsDigest): ReadTool<THost>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { resolveDependencies } from "./deps.js";
|
|
2
|
+
import { isRecord } from "./input.js";
|
|
3
|
+
import { runRead } from "./pipeline.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates the read tool. Throws TypeError on an unknown or malformed dependency,
|
|
6
|
+
* a missing `fs`, an empty `classifiers` list, or a `state` without a `digest`.
|
|
7
|
+
*/
|
|
8
|
+
export function createReadTool(deps) {
|
|
9
|
+
const resolved = resolveDependencies(deps);
|
|
10
|
+
const read = async (input, ctx) => {
|
|
11
|
+
if (ctx !== undefined && !isRecord(ctx)) {
|
|
12
|
+
throw new TypeError("read context must be an object");
|
|
13
|
+
}
|
|
14
|
+
const call = ctx ?? {};
|
|
15
|
+
return runRead(resolved, input, call);
|
|
16
|
+
};
|
|
17
|
+
return read;
|
|
18
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class AbortReadError extends Error {
|
|
2
|
+
}
|
|
3
|
+
export declare class ByteCursor {
|
|
4
|
+
private iterator;
|
|
5
|
+
private chunk;
|
|
6
|
+
private pushed;
|
|
7
|
+
private offset;
|
|
8
|
+
constructor(source: AsyncIterable<Uint8Array>);
|
|
9
|
+
unshift(piece: Uint8Array): void;
|
|
10
|
+
next(signal: AbortSignal | undefined): Promise<IteratorResult<Uint8Array>>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
private pull;
|
|
13
|
+
}
|
|
14
|
+
export declare function raceAbort<T>(start: () => T | Promise<T>, signal: AbortSignal | undefined): Promise<T>;
|