@oscharko-dev/keiko-workspace 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/dist/.tsbuildinfo +1 -0
- package/dist/binaryDetect.d.ts +6 -0
- package/dist/binaryDetect.d.ts.map +1 -0
- package/dist/binaryDetect.js +20 -0
- package/dist/contextPack.d.ts +24 -0
- package/dist/contextPack.d.ts.map +1 -0
- package/dist/contextPack.js +118 -0
- package/dist/detect.d.ts +5 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +144 -0
- package/dist/discovery.d.ts +10 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +199 -0
- package/dist/document-extraction.d.ts +44 -0
- package/dist/document-extraction.d.ts.map +1 -0
- package/dist/document-extraction.js +372 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +4 -0
- package/dist/fs.d.ts +25 -0
- package/dist/fs.d.ts.map +1 -0
- package/dist/fs.js +69 -0
- package/dist/gitHistory.d.ts +3 -0
- package/dist/gitHistory.d.ts.map +1 -0
- package/dist/gitHistory.js +317 -0
- package/dist/ignore.d.ts +15 -0
- package/dist/ignore.d.ts.map +1 -0
- package/dist/ignore.js +248 -0
- package/dist/importGraph.d.ts +3 -0
- package/dist/importGraph.d.ts.map +1 -0
- package/dist/importGraph.js +131 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +38 -0
- package/dist/realpath.d.ts +9 -0
- package/dist/realpath.d.ts.map +1 -0
- package/dist/realpath.js +72 -0
- package/dist/repoSearch.d.ts +46 -0
- package/dist/repoSearch.d.ts.map +1 -0
- package/dist/repoSearch.js +350 -0
- package/dist/repoSearchEntries.d.ts +15 -0
- package/dist/repoSearchEntries.d.ts.map +1 -0
- package/dist/repoSearchEntries.js +106 -0
- package/dist/repoSearchLineSelection.d.ts +18 -0
- package/dist/repoSearchLineSelection.d.ts.map +1 -0
- package/dist/repoSearchLineSelection.js +43 -0
- package/dist/repoSearchMatchers.d.ts +8 -0
- package/dist/repoSearchMatchers.d.ts.map +1 -0
- package/dist/repoSearchMatchers.js +414 -0
- package/dist/repoSearchPolicy.d.ts +34 -0
- package/dist/repoSearchPolicy.d.ts.map +1 -0
- package/dist/repoSearchPolicy.js +342 -0
- package/dist/repoSearchRegexSafety.d.ts +2 -0
- package/dist/repoSearchRegexSafety.d.ts.map +1 -0
- package/dist/repoSearchRegexSafety.js +15 -0
- package/dist/repoSearchScan.d.ts +62 -0
- package/dist/repoSearchScan.d.ts.map +1 -0
- package/dist/repoSearchScan.js +292 -0
- package/dist/retrieval.d.ts +10 -0
- package/dist/retrieval.d.ts.map +1 -0
- package/dist/retrieval.js +74 -0
- package/dist/stableId.d.ts +4 -0
- package/dist/stableId.d.ts.map +1 -0
- package/dist/stableId.js +49 -0
- package/dist/structuralAdapters.d.ts +27 -0
- package/dist/structuralAdapters.d.ts.map +1 -0
- package/dist/structuralAdapters.js +87 -0
- package/dist/summary.d.ts +4 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +54 -0
- package/dist/testSourcePairing.d.ts +3 -0
- package/dist/testSourcePairing.d.ts.map +1 -0
- package/dist/testSourcePairing.js +179 -0
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +4 -0
- package/package.json +35 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// Recursive, bounded, deterministic file discovery and a single boundary-checked read path.
|
|
2
|
+
// Security invariants (ADR-0005 D2/D3):
|
|
3
|
+
// - every directory descent and every read goes through resolveWithinWorkspace first;
|
|
4
|
+
// - always-on DENY patterns are applied before the optional .gitignore subset;
|
|
5
|
+
// - a symlink whose realpath escapes the root is skipped (never followed);
|
|
6
|
+
// - recursion is capped by maxDepth and total results by maxFiles.
|
|
7
|
+
import { relative } from "node:path";
|
|
8
|
+
import { nodeWorkspaceFs, } from "./fs.js";
|
|
9
|
+
import { compileIgnore, isDenied, isIgnored } from "./ignore.js";
|
|
10
|
+
import { resolveWithinWorkspace } from "./paths.js";
|
|
11
|
+
import { containedRealPathInfo } from "./realpath.js";
|
|
12
|
+
import { FileTooLargeError, PathDeniedError, WorkspaceReadError } from "./errors.js";
|
|
13
|
+
import { redact } from "@oscharko-dev/keiko-security";
|
|
14
|
+
import { DEFAULT_READ_OPTIONS, } from "./types.js";
|
|
15
|
+
function toRelative(root, absolutePath) {
|
|
16
|
+
return relative(root, absolutePath).split("\\").join("/");
|
|
17
|
+
}
|
|
18
|
+
function toRealRelative(fs, root, absolutePath) {
|
|
19
|
+
try {
|
|
20
|
+
return toRelative(fs.realPath(root), absolutePath);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return toRelative(root, absolutePath);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// A benign-named workspace root that is a SYMLINK into a denied location (e.g. "~/docs" -> "~/.aws")
|
|
27
|
+
// is invisible to the root-relative deny checks: they only see paths BELOW the root, never the denied
|
|
28
|
+
// segment that resolving the root itself introduces. Flag it — but ONLY when the symlink ADDS the
|
|
29
|
+
// denial (the real root is denied while the lexical root is not). A non-symlinked root, a benign
|
|
30
|
+
// platform link (macOS "/tmp" -> "/private/tmp"), and a root that merely sits under a denied-named
|
|
31
|
+
// ANCESTOR the user did not symlink into (e.g. the product's own ".claude" worktree) all keep working,
|
|
32
|
+
// preserving the existing relative-only semantics for every non-attack path.
|
|
33
|
+
function realRootIsDeniedViaSymlink(realRoot, lexicalRoot) {
|
|
34
|
+
return isDenied(realRoot) && !isDenied(lexicalRoot);
|
|
35
|
+
}
|
|
36
|
+
// Returns false when the entry must be skipped for any security or noise reason, recording
|
|
37
|
+
// which tier rejected it for the discovery stats.
|
|
38
|
+
function isAllowed(walk, relPath, isDir) {
|
|
39
|
+
if (isDenied(relPath)) {
|
|
40
|
+
walk.denied += 1;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
if (walk.applyGitignore && isIgnored(walk.matcher, relPath, isDir)) {
|
|
44
|
+
walk.ignored += 1;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
function childRelative(root, absoluteDir, name) {
|
|
50
|
+
const dirRel = toRelative(root, absoluteDir);
|
|
51
|
+
return dirRel === "" ? name : `${dirRel}/${name}`;
|
|
52
|
+
}
|
|
53
|
+
function readDirSafe(walk, absoluteDir) {
|
|
54
|
+
try {
|
|
55
|
+
return walk.fs.readDir(absoluteDir);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function statSize(walk, absolutePath) {
|
|
62
|
+
try {
|
|
63
|
+
return walk.fs.stat(absolutePath).size;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function handleEntry(walk, absoluteDir, entry, depth) {
|
|
70
|
+
const childAbs = resolveWithinWorkspace(walk.root, childRelative(walk.root, absoluteDir, entry.name));
|
|
71
|
+
const relPath = toRelative(walk.root, childAbs);
|
|
72
|
+
if (!isAllowed(walk, relPath, entry.isDirectory)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Symlinks are skipped unconditionally (for safety/simplicity). A non-symlink entry that
|
|
76
|
+
// reports neither isFile nor isDirectory is likewise treated as non-traversable noise.
|
|
77
|
+
// Only genuine files and directories are walked.
|
|
78
|
+
if (entry.isSymbolicLink) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (entry.isDirectory) {
|
|
82
|
+
descend(walk, childAbs, depth + 1);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (entry.isFile) {
|
|
86
|
+
walk.out.push({ relativePath: relPath, sizeBytes: statSize(walk, childAbs) });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function descend(walk, absoluteDir, depth) {
|
|
90
|
+
if (depth > walk.opts.maxDepth || walk.out.length >= walk.opts.maxFiles) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const entries = [...readDirSafe(walk, absoluteDir)].sort((a, b) => (a.name < b.name ? -1 : 1));
|
|
94
|
+
for (const entry of entries) {
|
|
95
|
+
if (walk.out.length >= walk.opts.maxFiles) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
handleEntry(walk, absoluteDir, entry, depth);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function runWalk(workspace, opts, fs) {
|
|
102
|
+
const walk = {
|
|
103
|
+
fs,
|
|
104
|
+
root: workspace.root,
|
|
105
|
+
matcher: compileIgnore(workspace.ignoreLines),
|
|
106
|
+
opts,
|
|
107
|
+
applyGitignore: opts.applyGitignore,
|
|
108
|
+
out: [],
|
|
109
|
+
denied: 0,
|
|
110
|
+
ignored: 0,
|
|
111
|
+
};
|
|
112
|
+
// Refuse to walk a benign-named root that resolves into a denied location via a symlink: discovery
|
|
113
|
+
// does not realpath-contain the ROOT, so it would otherwise list a symlinked credential dir's files.
|
|
114
|
+
// Treated as denied (no throw — discovery filters rather than raises), consistent with per-entry deny.
|
|
115
|
+
let realRoot;
|
|
116
|
+
try {
|
|
117
|
+
realRoot = fs.realPath(workspace.root);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
realRoot = workspace.root;
|
|
121
|
+
}
|
|
122
|
+
if (realRootIsDeniedViaSymlink(realRoot, workspace.root)) {
|
|
123
|
+
walk.denied += 1;
|
|
124
|
+
return walk;
|
|
125
|
+
}
|
|
126
|
+
descend(walk, resolveWithinWorkspace(workspace.root, "."), 0);
|
|
127
|
+
return walk;
|
|
128
|
+
}
|
|
129
|
+
export function discoverFiles(workspace, opts, fs = nodeWorkspaceFs) {
|
|
130
|
+
return runWalk(workspace, opts, fs).out;
|
|
131
|
+
}
|
|
132
|
+
export function discoverWithStats(workspace, opts, fs = nodeWorkspaceFs) {
|
|
133
|
+
const walk = runWalk(workspace, opts, fs);
|
|
134
|
+
return {
|
|
135
|
+
files: walk.out,
|
|
136
|
+
stats: { discovered: walk.out.length, denied: walk.denied, ignored: walk.ignored },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function describe(error) {
|
|
140
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
141
|
+
}
|
|
142
|
+
function statFile(fs, absolutePath, relPath) {
|
|
143
|
+
try {
|
|
144
|
+
return fs.stat(absolutePath);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new WorkspaceReadError(`cannot stat file: ${relPath} (${describe(error)})`, relPath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function assertNoHardLinkAlias(stats, relPath) {
|
|
151
|
+
if (stats.hardLinkCount !== undefined && stats.hardLinkCount > 1) {
|
|
152
|
+
throw new PathDeniedError(`refusing to read a hard-linked workspace alias: ${relPath}`, relPath);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function readContent(fs, absolutePath, relPath, opts) {
|
|
156
|
+
let raw;
|
|
157
|
+
try {
|
|
158
|
+
raw = fs.readFileUtf8(absolutePath);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new WorkspaceReadError(`cannot read file: ${relPath} (${describe(error)})`, relPath);
|
|
162
|
+
}
|
|
163
|
+
const rawBytes = Buffer.byteLength(raw, "utf8");
|
|
164
|
+
const truncated = rawBytes > opts.maxBytes;
|
|
165
|
+
const text = truncated
|
|
166
|
+
? Buffer.from(raw, "utf8").subarray(0, opts.maxBytes).toString("utf8")
|
|
167
|
+
: raw;
|
|
168
|
+
return { relativePath: relPath, sizeBytes: rawBytes, text: redact(text), truncated };
|
|
169
|
+
}
|
|
170
|
+
// The single read path. Order: boundary -> deny -> realpath containment -> size cap -> read -> redact.
|
|
171
|
+
// Realpath containment is shared with the write/cwd paths via assertContainedRealPath: when the
|
|
172
|
+
// path does not exist, it validates the nearest existing parent and returns absolutePath, so a
|
|
173
|
+
// missing in-root file still surfaces as a WorkspaceReadError (not a false PathEscapeError).
|
|
174
|
+
export function readWorkspaceFile(workspace, relPath, opts = DEFAULT_READ_OPTIONS, fs = nodeWorkspaceFs) {
|
|
175
|
+
const absolutePath = resolveWithinWorkspace(workspace.root, relPath);
|
|
176
|
+
const normalizedRel = toRelative(workspace.root, absolutePath);
|
|
177
|
+
if (isDenied(normalizedRel)) {
|
|
178
|
+
throw new PathDeniedError(`refusing to read a denied path: ${normalizedRel}`, normalizedRel);
|
|
179
|
+
}
|
|
180
|
+
const contained = containedRealPathInfo(fs, workspace.root, absolutePath);
|
|
181
|
+
// Deny a benign-named root symlink that resolves into a protected location (e.g. "~/docs" ->
|
|
182
|
+
// "~/.aws"): the deny checks here only see the path relative to the realpath'd root, so a denied
|
|
183
|
+
// segment in the ROOT itself is invisible to them and the file would read through. Only the symlink
|
|
184
|
+
// case is added — see realRootIsDeniedViaSymlink — so existing non-symlink reads are unchanged.
|
|
185
|
+
if (realRootIsDeniedViaSymlink(contained.realBase, workspace.root)) {
|
|
186
|
+
throw new PathDeniedError(`refusing to read a denied path: ${normalizedRel}`, normalizedRel);
|
|
187
|
+
}
|
|
188
|
+
const resolvedPath = contained.path;
|
|
189
|
+
const resolvedRel = toRealRelative(fs, workspace.root, resolvedPath);
|
|
190
|
+
if (isDenied(resolvedRel)) {
|
|
191
|
+
throw new PathDeniedError(`refusing to read a denied path: ${normalizedRel}`, normalizedRel);
|
|
192
|
+
}
|
|
193
|
+
const stats = statFile(fs, resolvedPath, normalizedRel);
|
|
194
|
+
assertNoHardLinkAlias(stats, normalizedRel);
|
|
195
|
+
if (stats.size > opts.maxBytes) {
|
|
196
|
+
throw new FileTooLargeError(`file exceeds the read cap: ${normalizedRel}`, normalizedRel, stats.size, opts.maxBytes);
|
|
197
|
+
}
|
|
198
|
+
return readContent(fs, resolvedPath, normalizedRel, opts);
|
|
199
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { WorkspaceFs } from "./fs.js";
|
|
2
|
+
export declare const MAX_EXTRACTED_BYTES = 65536;
|
|
3
|
+
export declare const MAX_TOTAL_EXTRACTED_BYTES = 262144;
|
|
4
|
+
export declare const SUPPORTED_MIME_PREFIXES: readonly string[];
|
|
5
|
+
export declare const SUPPORTED_MIME_LITERALS: ReadonlySet<string>;
|
|
6
|
+
export type DocumentExtractionFailure = {
|
|
7
|
+
readonly kind: "binary-file";
|
|
8
|
+
readonly mimeHint?: string | undefined;
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "unsupported-type";
|
|
11
|
+
readonly mimeHint?: string | undefined;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "denied-path";
|
|
14
|
+
} | {
|
|
15
|
+
readonly kind: "not-found";
|
|
16
|
+
} | {
|
|
17
|
+
readonly kind: "unreadable";
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "empty";
|
|
20
|
+
};
|
|
21
|
+
export interface ExtractedDocumentContext {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
readonly displayName: string;
|
|
24
|
+
readonly mimeType: string;
|
|
25
|
+
readonly sizeBytes: number;
|
|
26
|
+
readonly extractedBytes: number;
|
|
27
|
+
readonly truncated: boolean;
|
|
28
|
+
readonly truncationMarker: string | undefined;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DocumentExtractionBudget {
|
|
32
|
+
readonly perDocBytes: number;
|
|
33
|
+
readonly totalBudgetUsedBytes: number;
|
|
34
|
+
readonly totalBudgetBytes: number;
|
|
35
|
+
}
|
|
36
|
+
export type DocumentExtractionResult = {
|
|
37
|
+
readonly ok: true;
|
|
38
|
+
readonly context: ExtractedDocumentContext;
|
|
39
|
+
} | {
|
|
40
|
+
readonly ok: false;
|
|
41
|
+
readonly failure: DocumentExtractionFailure;
|
|
42
|
+
};
|
|
43
|
+
export declare function extractDocumentContext(fs: WorkspaceFs, workspaceRoot: string, relativePath: string, budget: DocumentExtractionBudget): Promise<DocumentExtractionResult>;
|
|
44
|
+
//# sourceMappingURL=document-extraction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document-extraction.d.ts","sourceRoot":"","sources":["../src/document-extraction.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAK3C,eAAO,MAAM,mBAAmB,QAAS,CAAC;AAC1C,eAAO,MAAM,yBAAyB,SAAU,CAAC;AAEjD,eAAO,MAAM,uBAAuB,EAAE,SAAS,MAAM,EAAc,CAAC;AAEpE,eAAO,MAAM,uBAAuB,EAAE,WAAW,CAAC,MAAM,CAOtD,CAAC;AA8CH,MAAM,MAAM,yBAAyB,GACjC;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GACxE;IAAE,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC7E;IAAE,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAA;CAAE,GAChC;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAA;CAAE,GAC/B;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC;AAE/B,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;CACnC;AAED,MAAM,MAAM,wBAAwB,GAChC;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,wBAAwB,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,yBAAyB,CAAA;CAAE,CAAC;AAwSxE,wBAAsB,sBAAsB,CAC1C,EAAE,EAAE,WAAW,EACf,aAAa,EAAE,MAAM,EACrB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,wBAAwB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAMnC"}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Issue #148 — Safe document context extraction for conversation inputs (Epic #142).
|
|
2
|
+
//
|
|
3
|
+
// This module turns a workspace-relative file path into a bounded, redacted text excerpt that
|
|
4
|
+
// the BFF and the model gateway can safely concatenate into a prompt. The extractor is text-only
|
|
5
|
+
// by deliberate design: PDF, Word, and other binary document parsing is OUT OF SCOPE for this
|
|
6
|
+
// issue because it would require a new parser dependency, a much larger trust surface (CVE-risk
|
|
7
|
+
// in parsing libraries), and an OCR strategy that #148 does not own.
|
|
8
|
+
//
|
|
9
|
+
// Byte-budget rationale (matches the per-payload aggregate budget on the server side):
|
|
10
|
+
// - Per-document cap of 64 KiB (MAX_EXTRACTED_BYTES) is large enough to carry a typical
|
|
11
|
+
// README/spec/JSON config in full and small enough that 4 attached files at the per-payload
|
|
12
|
+
// aggregate cap of 256 KiB (MAX_TOTAL_EXTRACTED_BYTES) still fits inside the gateway's
|
|
13
|
+
// 128 K-character body cap (MAX_BODY_BYTES) with room for the user draft and JSON framing.
|
|
14
|
+
// - Truncation is REPORTED to the caller (`truncated: true` + human-readable marker) so the
|
|
15
|
+
// UI can render a badge and the prompt composer can append a fixed marker after the text.
|
|
16
|
+
//
|
|
17
|
+
// Path-safe error contract (AC #2):
|
|
18
|
+
// - The failure tagged-union carries a `kind` ONLY. No `path` field; no message field that
|
|
19
|
+
// embeds the resolved or relative path. This keeps absolute filesystem paths off the wire
|
|
20
|
+
// for both reportable failures and unreportable ones (binary/empty/etc.).
|
|
21
|
+
// - All four boundary errors (denied-path / not-found / unreadable / binary-file) are derived
|
|
22
|
+
// from the existing workspace primitives (`resolveWithinWorkspace`,
|
|
23
|
+
// `assertContainedRealPath`, `looksBinary`) so this module owns no new path-validation logic.
|
|
24
|
+
import { basename, extname } from "node:path";
|
|
25
|
+
import { randomUUID } from "node:crypto";
|
|
26
|
+
import { redact } from "@oscharko-dev/keiko-security";
|
|
27
|
+
import { looksBinary } from "./binaryDetect.js";
|
|
28
|
+
import { PathEscapeError, PathDeniedError } from "./errors.js";
|
|
29
|
+
import { resolveWithinWorkspace } from "./paths.js";
|
|
30
|
+
import { assertContainedRealPath } from "./realpath.js";
|
|
31
|
+
import { isDenied } from "./ignore.js";
|
|
32
|
+
export const MAX_EXTRACTED_BYTES = 65_536; // per-document budget (64 KiB)
|
|
33
|
+
export const MAX_TOTAL_EXTRACTED_BYTES = 262_144; // per-payload aggregate budget (256 KiB)
|
|
34
|
+
export const SUPPORTED_MIME_PREFIXES = ["text/"];
|
|
35
|
+
export const SUPPORTED_MIME_LITERALS = new Set([
|
|
36
|
+
"application/json",
|
|
37
|
+
"application/x-yaml",
|
|
38
|
+
"application/yaml",
|
|
39
|
+
"application/xml",
|
|
40
|
+
"application/javascript",
|
|
41
|
+
"application/typescript",
|
|
42
|
+
]);
|
|
43
|
+
// File-extension → MIME map. Only text-like / structured-text formats are recognised. Anything
|
|
44
|
+
// outside this map falls into `unsupported-type` (e.g. `.exe`, `.pdf`, `.docx`, `.png`).
|
|
45
|
+
const EXTENSION_MIME = new Map([
|
|
46
|
+
[".md", "text/markdown"],
|
|
47
|
+
[".markdown", "text/markdown"],
|
|
48
|
+
[".txt", "text/plain"],
|
|
49
|
+
[".log", "text/plain"],
|
|
50
|
+
[".json", "application/json"],
|
|
51
|
+
[".yaml", "application/yaml"],
|
|
52
|
+
[".yml", "application/yaml"],
|
|
53
|
+
[".xml", "application/xml"],
|
|
54
|
+
[".html", "text/html"],
|
|
55
|
+
[".htm", "text/html"],
|
|
56
|
+
[".css", "text/css"],
|
|
57
|
+
[".js", "application/javascript"],
|
|
58
|
+
[".jsx", "application/javascript"],
|
|
59
|
+
[".mjs", "application/javascript"],
|
|
60
|
+
[".cjs", "application/javascript"],
|
|
61
|
+
[".ts", "application/typescript"],
|
|
62
|
+
[".tsx", "application/typescript"],
|
|
63
|
+
[".py", "text/x-python"],
|
|
64
|
+
[".rb", "text/x-ruby"],
|
|
65
|
+
[".go", "text/x-go"],
|
|
66
|
+
[".rs", "text/x-rust"],
|
|
67
|
+
[".java", "text/x-java"],
|
|
68
|
+
[".kt", "text/x-kotlin"],
|
|
69
|
+
[".cpp", "text/x-c++src"],
|
|
70
|
+
[".cc", "text/x-c++src"],
|
|
71
|
+
[".cxx", "text/x-c++src"],
|
|
72
|
+
[".c", "text/x-csrc"],
|
|
73
|
+
[".h", "text/x-chdr"],
|
|
74
|
+
[".hpp", "text/x-c++hdr"],
|
|
75
|
+
[".sh", "text/x-shellscript"],
|
|
76
|
+
[".bash", "text/x-shellscript"],
|
|
77
|
+
[".zsh", "text/x-shellscript"],
|
|
78
|
+
[".toml", "application/toml"],
|
|
79
|
+
[".ini", "text/plain"],
|
|
80
|
+
[".csv", "text/csv"],
|
|
81
|
+
[".tsv", "text/tab-separated-values"],
|
|
82
|
+
[".sql", "application/sql"],
|
|
83
|
+
]);
|
|
84
|
+
const BINARY_PROBE_BYTES = 512;
|
|
85
|
+
function classifyByExtension(relativePath) {
|
|
86
|
+
const ext = extname(relativePath).toLowerCase();
|
|
87
|
+
if (ext.length === 0) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
return EXTENSION_MIME.get(ext);
|
|
91
|
+
}
|
|
92
|
+
function isSupportedMime(mimeType) {
|
|
93
|
+
if (SUPPORTED_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix))) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
return SUPPORTED_MIME_LITERALS.has(mimeType);
|
|
97
|
+
}
|
|
98
|
+
function denied() {
|
|
99
|
+
return { ok: false, failure: { kind: "denied-path" } };
|
|
100
|
+
}
|
|
101
|
+
function notFound() {
|
|
102
|
+
return { ok: false, failure: { kind: "not-found" } };
|
|
103
|
+
}
|
|
104
|
+
function unreadable() {
|
|
105
|
+
return { ok: false, failure: { kind: "unreadable" } };
|
|
106
|
+
}
|
|
107
|
+
function empty() {
|
|
108
|
+
return { ok: false, failure: { kind: "empty" } };
|
|
109
|
+
}
|
|
110
|
+
function binary(mimeHint) {
|
|
111
|
+
if (mimeHint === undefined) {
|
|
112
|
+
return { ok: false, failure: { kind: "binary-file" } };
|
|
113
|
+
}
|
|
114
|
+
return { ok: false, failure: { kind: "binary-file", mimeHint } };
|
|
115
|
+
}
|
|
116
|
+
function unsupported(mimeHint) {
|
|
117
|
+
if (mimeHint === undefined) {
|
|
118
|
+
return { ok: false, failure: { kind: "unsupported-type" } };
|
|
119
|
+
}
|
|
120
|
+
return { ok: false, failure: { kind: "unsupported-type", mimeHint } };
|
|
121
|
+
}
|
|
122
|
+
function isStepOk(value) {
|
|
123
|
+
return "step" in value;
|
|
124
|
+
}
|
|
125
|
+
function resolveSafePath(fs, workspaceRoot, relativePath) {
|
|
126
|
+
let absolutePath;
|
|
127
|
+
try {
|
|
128
|
+
absolutePath = resolveWithinWorkspace(workspaceRoot, relativePath);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (error instanceof PathEscapeError) {
|
|
132
|
+
return denied();
|
|
133
|
+
}
|
|
134
|
+
throw error;
|
|
135
|
+
}
|
|
136
|
+
const normalizedRel = absolutePath.slice(workspaceRoot.length).replace(/^[/\\]/, "");
|
|
137
|
+
if (isDenied(normalizedRel)) {
|
|
138
|
+
return denied();
|
|
139
|
+
}
|
|
140
|
+
let resolved;
|
|
141
|
+
try {
|
|
142
|
+
resolved = assertContainedRealPath(fs, workspaceRoot, absolutePath, normalizedRel);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
if (error instanceof PathEscapeError || error instanceof PathDeniedError) {
|
|
146
|
+
return denied();
|
|
147
|
+
}
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
return { step: "ok", resolved };
|
|
151
|
+
}
|
|
152
|
+
function statFile(fs, resolvedPath) {
|
|
153
|
+
try {
|
|
154
|
+
const stats = fs.stat(resolvedPath);
|
|
155
|
+
return { step: "ok", size: stats.size, isFile: stats.isFile };
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
if (!fs.exists(resolvedPath)) {
|
|
159
|
+
return notFound();
|
|
160
|
+
}
|
|
161
|
+
return unreadable();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function effectivePerDocBudget(budget) {
|
|
165
|
+
const remainingTotal = Math.max(0, budget.totalBudgetBytes - budget.totalBudgetUsedBytes);
|
|
166
|
+
return Math.min(budget.perDocBytes, remainingTotal);
|
|
167
|
+
}
|
|
168
|
+
async function probeBinary(fs, resolvedPath, size) {
|
|
169
|
+
if (fs.readFileBytes === undefined) {
|
|
170
|
+
// Synchronous read fallback for FS adapters without the byte-level port. We only need a
|
|
171
|
+
// small slice for the binary probe; reading utf-8-as-string and re-encoding is acceptable
|
|
172
|
+
// here because EVERY adapter ships readFileUtf8 (it's a required port member).
|
|
173
|
+
let utf8;
|
|
174
|
+
try {
|
|
175
|
+
utf8 = fs.readFileUtf8(resolvedPath);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return unreadable();
|
|
179
|
+
}
|
|
180
|
+
const encoded = new TextEncoder().encode(utf8);
|
|
181
|
+
return {
|
|
182
|
+
step: "ok",
|
|
183
|
+
bytes: encoded.subarray(0, Math.min(BINARY_PROBE_BYTES, encoded.length)),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
const bytes = await fs.readFileBytes(resolvedPath, Math.min(BINARY_PROBE_BYTES, size));
|
|
188
|
+
return { step: "ok", bytes };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return unreadable();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function readBudgetedBytes(fs, resolvedPath, cap) {
|
|
195
|
+
if (cap === 0) {
|
|
196
|
+
return { step: "ok", bytes: new Uint8Array(0) };
|
|
197
|
+
}
|
|
198
|
+
if (fs.readFileBytes !== undefined) {
|
|
199
|
+
try {
|
|
200
|
+
const bytes = await fs.readFileBytes(resolvedPath, cap);
|
|
201
|
+
return { step: "ok", bytes };
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return unreadable();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
let utf8;
|
|
208
|
+
try {
|
|
209
|
+
utf8 = fs.readFileUtf8(resolvedPath);
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
return unreadable();
|
|
213
|
+
}
|
|
214
|
+
const encoded = new TextEncoder().encode(utf8);
|
|
215
|
+
return { step: "ok", bytes: encoded.subarray(0, Math.min(cap, encoded.length)) };
|
|
216
|
+
}
|
|
217
|
+
function buildTruncationMarker(extractedBytes, originalBytes) {
|
|
218
|
+
return `[…truncated to first ${String(extractedBytes)} of ${String(originalBytes)} bytes]`;
|
|
219
|
+
}
|
|
220
|
+
// Returns the expected byte-length of the UTF-8 sequence starting with `lead`, or 0 when
|
|
221
|
+
// the byte is not a valid UTF-8 leading byte.
|
|
222
|
+
function utf8LeadByteSeqLen(lead) {
|
|
223
|
+
if ((lead & 0x80) === 0x00)
|
|
224
|
+
return 1; // ASCII
|
|
225
|
+
if ((lead & 0xe0) === 0xc0)
|
|
226
|
+
return 2;
|
|
227
|
+
if ((lead & 0xf0) === 0xe0)
|
|
228
|
+
return 3;
|
|
229
|
+
if ((lead & 0xf8) === 0xf0)
|
|
230
|
+
return 4;
|
|
231
|
+
return 0; // continuation byte or invalid — not a lead byte
|
|
232
|
+
}
|
|
233
|
+
// Returns the length of the valid UTF-8 prefix of `bytes`, backing off any incomplete
|
|
234
|
+
// multibyte sequence at the tail. A full file that is valid UTF-8 will have its entire
|
|
235
|
+
// length returned unchanged; a capped slice that was cut mid-codepoint will have at most
|
|
236
|
+
// 3 bytes trimmed (the maximum tail of an incomplete 4-byte sequence).
|
|
237
|
+
//
|
|
238
|
+
// Algorithm: scan backward from the end for the first byte that is NOT a UTF-8 continuation
|
|
239
|
+
// byte (0x80–0xBF). That byte is the start of the last (possibly incomplete) sequence.
|
|
240
|
+
// If the sequence is incomplete, exclude it; otherwise keep the full slice.
|
|
241
|
+
function validUtf8PrefixLength(bytes) {
|
|
242
|
+
const len = bytes.length;
|
|
243
|
+
if (len === 0)
|
|
244
|
+
return 0;
|
|
245
|
+
// Walk back over continuation bytes (0x80–0xBF), up to 3.
|
|
246
|
+
let i = len - 1;
|
|
247
|
+
const limit = Math.max(len - 4, -1);
|
|
248
|
+
while (i > limit && ((bytes[i] ?? 0) & 0xc0) === 0x80) {
|
|
249
|
+
i -= 1;
|
|
250
|
+
}
|
|
251
|
+
const seqLen = utf8LeadByteSeqLen(bytes[i] ?? 0);
|
|
252
|
+
if (seqLen === 0)
|
|
253
|
+
return i; // not a lead byte — exclude it
|
|
254
|
+
// If the sequence started at i extends past the slice end, exclude it.
|
|
255
|
+
return i + seqLen <= len ? len : i;
|
|
256
|
+
}
|
|
257
|
+
function decodeUtf8(bytes) {
|
|
258
|
+
// fatal: true makes the decoder throw on invalid UTF-8 byte sequences. This is the
|
|
259
|
+
// second binary-classification gate after the NUL-byte probe — a file that survives the
|
|
260
|
+
// probe but is still not valid UTF-8 is treated as binary, not silently decoded with
|
|
261
|
+
// replacement characters.
|
|
262
|
+
try {
|
|
263
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
264
|
+
return { step: "ok", text: decoder.decode(bytes) };
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return binary();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function trimTrailingWhitespace(value) {
|
|
271
|
+
return value.replace(/\s+$/u, "");
|
|
272
|
+
}
|
|
273
|
+
async function classifyFileMime(fs, file, relativePath) {
|
|
274
|
+
const probe = await probeBinary(fs, file.resolvedPath, file.size);
|
|
275
|
+
if (!isStepOk(probe)) {
|
|
276
|
+
return probe;
|
|
277
|
+
}
|
|
278
|
+
if (looksBinary(probe.bytes)) {
|
|
279
|
+
return binary(classifyByExtension(relativePath));
|
|
280
|
+
}
|
|
281
|
+
const mimeType = classifyByExtension(relativePath);
|
|
282
|
+
if (mimeType === undefined || !isSupportedMime(mimeType)) {
|
|
283
|
+
return unsupported(mimeType);
|
|
284
|
+
}
|
|
285
|
+
return { step: "ok", mimeType };
|
|
286
|
+
}
|
|
287
|
+
async function readAndCap(fs, file, budget) {
|
|
288
|
+
const cap = effectivePerDocBudget(budget);
|
|
289
|
+
const read = await readBudgetedBytes(fs, file.resolvedPath, cap);
|
|
290
|
+
if (!isStepOk(read)) {
|
|
291
|
+
return read;
|
|
292
|
+
}
|
|
293
|
+
// When the byte slice was capped below the file size a multibyte codepoint may straddle
|
|
294
|
+
// the boundary. Back the slice to the last complete UTF-8 codepoint so the fatal decoder
|
|
295
|
+
// does not mistake a clean text file for binary. A file that is genuinely NOT at a
|
|
296
|
+
// codepoint boundary due to truncation will have at most 3 bytes trimmed.
|
|
297
|
+
const isCapped = read.bytes.length < file.size;
|
|
298
|
+
const bytes = isCapped ? read.bytes.subarray(0, validUtf8PrefixLength(read.bytes)) : read.bytes;
|
|
299
|
+
const decoded = decodeUtf8(bytes);
|
|
300
|
+
if (!isStepOk(decoded)) {
|
|
301
|
+
return decoded;
|
|
302
|
+
}
|
|
303
|
+
const text = trimTrailingWhitespace(decoded.text);
|
|
304
|
+
// Report the number of bytes actually read from disk (before the codepoint trim) so the
|
|
305
|
+
// truncation marker quotes an honest byte count rather than the post-trim length.
|
|
306
|
+
const extractedBytes = read.bytes.length;
|
|
307
|
+
const truncated = extractedBytes < file.size;
|
|
308
|
+
return { step: "ok", value: { text, extractedBytes, truncated } };
|
|
309
|
+
}
|
|
310
|
+
function buildContext(relativePath, mimeType, file, capped) {
|
|
311
|
+
const marker = capped.truncated
|
|
312
|
+
? buildTruncationMarker(capped.extractedBytes, file.size)
|
|
313
|
+
: undefined;
|
|
314
|
+
return {
|
|
315
|
+
id: randomUUID(),
|
|
316
|
+
displayName: basename(relativePath),
|
|
317
|
+
mimeType,
|
|
318
|
+
sizeBytes: file.size,
|
|
319
|
+
extractedBytes: capped.extractedBytes,
|
|
320
|
+
truncated: capped.truncated,
|
|
321
|
+
truncationMarker: marker,
|
|
322
|
+
text: redact(capped.text),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
// Public entry: extracts text from a workspace-relative path. All error paths produce a path-
|
|
326
|
+
// safe DocumentExtractionFailure tagged-union (no path strings in the failure object).
|
|
327
|
+
export async function extractDocumentContext(fs, workspaceRoot, relativePath, budget) {
|
|
328
|
+
const safe = resolveSafePath(fs, workspaceRoot, relativePath);
|
|
329
|
+
if (isStepOk(safe)) {
|
|
330
|
+
return extractFromResolvedPath(fs, relativePath, safe.resolved, budget);
|
|
331
|
+
}
|
|
332
|
+
return safe;
|
|
333
|
+
}
|
|
334
|
+
async function extractFromResolvedPath(fs, relativePath, resolvedPath, budget) {
|
|
335
|
+
const stat = statFile(fs, resolvedPath);
|
|
336
|
+
if (!isStepOk(stat)) {
|
|
337
|
+
return stat;
|
|
338
|
+
}
|
|
339
|
+
if (!stat.isFile) {
|
|
340
|
+
return notFound();
|
|
341
|
+
}
|
|
342
|
+
if (stat.size === 0) {
|
|
343
|
+
return empty();
|
|
344
|
+
}
|
|
345
|
+
const file = { resolvedPath, size: stat.size };
|
|
346
|
+
const mimeResult = await classifyFileMime(fs, file, relativePath);
|
|
347
|
+
if (!isStepOk(mimeResult)) {
|
|
348
|
+
return mimeResult;
|
|
349
|
+
}
|
|
350
|
+
const capped = await readAndCap(fs, file, budget);
|
|
351
|
+
if (!isStepOk(capped)) {
|
|
352
|
+
return capped;
|
|
353
|
+
}
|
|
354
|
+
if (capped.value.extractedBytes === 0) {
|
|
355
|
+
// Budget exhausted at entry: surface as a truncated zero-byte excerpt so the caller can
|
|
356
|
+
// still render a chip + truncation badge. AC #3 — UI shows the doc contributed nothing.
|
|
357
|
+
return {
|
|
358
|
+
ok: true,
|
|
359
|
+
context: {
|
|
360
|
+
id: randomUUID(),
|
|
361
|
+
displayName: basename(relativePath),
|
|
362
|
+
mimeType: mimeResult.mimeType,
|
|
363
|
+
sizeBytes: file.size,
|
|
364
|
+
extractedBytes: 0,
|
|
365
|
+
truncated: true,
|
|
366
|
+
truncationMarker: buildTruncationMarker(0, file.size),
|
|
367
|
+
text: "",
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return { ok: true, context: buildContext(relativePath, mimeResult.mimeType, file, capped.value) };
|
|
372
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { WORKSPACE_CODES, WorkspaceError, PathEscapeError, PathDeniedError, WorkspaceNotFoundError, FileTooLargeError, WorkspaceReadError, RepoSearchInvalidQueryError, RepoSearchInvalidRangeError, RepoSearchUnsupportedFileError, } from "@oscharko-dev/keiko-security/errors/workspace";
|
|
2
|
+
export type { WorkspaceCode } from "@oscharko-dev/keiko-security/errors/workspace";
|
|
3
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,eAAe,EACf,cAAc,EACd,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,2BAA2B,EAC3B,2BAA2B,EAC3B,8BAA8B,GAC/B,MAAM,+CAA+C,CAAC;AACvD,YAAY,EAAE,aAAa,EAAE,MAAM,+CAA+C,CAAC"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Re-export shim: the workspace error taxonomy now lives in @oscharko-dev/keiko-security
|
|
2
|
+
// (issue #159, ADR-0019). All existing import sites (`from "./errors.js"`) keep resolving
|
|
3
|
+
// unchanged via this barrel.
|
|
4
|
+
export { WORKSPACE_CODES, WorkspaceError, PathEscapeError, PathDeniedError, WorkspaceNotFoundError, FileTooLargeError, WorkspaceReadError, RepoSearchInvalidQueryError, RepoSearchInvalidRangeError, RepoSearchUnsupportedFileError, } from "@oscharko-dev/keiko-security/errors/workspace";
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface WorkspaceStat {
|
|
2
|
+
readonly size: number;
|
|
3
|
+
readonly isFile: boolean;
|
|
4
|
+
readonly isDirectory: boolean;
|
|
5
|
+
readonly isSymbolicLink: boolean;
|
|
6
|
+
readonly hardLinkCount?: number | undefined;
|
|
7
|
+
readonly mtimeMs?: number | undefined;
|
|
8
|
+
}
|
|
9
|
+
export interface WorkspaceDirEntry {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly isDirectory: boolean;
|
|
12
|
+
readonly isFile: boolean;
|
|
13
|
+
readonly isSymbolicLink: boolean;
|
|
14
|
+
}
|
|
15
|
+
export interface WorkspaceFs {
|
|
16
|
+
readonly readFileUtf8: (absolutePath: string) => string;
|
|
17
|
+
readonly stat: (absolutePath: string) => WorkspaceStat;
|
|
18
|
+
readonly readDir: (absolutePath: string) => readonly WorkspaceDirEntry[];
|
|
19
|
+
readonly realPath: (absolutePath: string) => string;
|
|
20
|
+
readonly exists: (absolutePath: string) => boolean;
|
|
21
|
+
readonly readFileBytes?: (absolutePath: string, maxBytes: number) => Promise<Uint8Array>;
|
|
22
|
+
readonly readFileRange?: (absolutePath: string, startByte: number, length: number) => Promise<Uint8Array>;
|
|
23
|
+
}
|
|
24
|
+
export declare const nodeWorkspaceFs: WorkspaceFs;
|
|
25
|
+
//# sourceMappingURL=fs.d.ts.map
|
package/dist/fs.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../src/fs.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5C,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD,QAAQ,CAAC,IAAI,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,aAAa,CAAC;IACvD,QAAQ,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,SAAS,iBAAiB,EAAE,CAAC;IACzE,QAAQ,CAAC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;IAInD,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;IAGzF,QAAQ,CAAC,aAAa,CAAC,EAAE,CACvB,YAAY,EAAE,MAAM,EACpB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,KACX,OAAO,CAAC,UAAU,CAAC,CAAC;CAC1B;AAMD,eAAO,MAAM,eAAe,EAAE,WA6D7B,CAAC"}
|