@j0hanz/filesystem-mcp 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -26
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +10 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +10 -3
- package/dist/cli.js.map +1 -1
- package/dist/core/concurrency.d.ts +4 -4
- package/dist/core/concurrency.d.ts.map +1 -1
- package/dist/core/concurrency.js +1 -1
- package/dist/core/concurrency.js.map +1 -1
- package/dist/core/config.d.ts +2 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/core/cursor.d.ts +8 -0
- package/dist/core/cursor.d.ts.map +1 -1
- package/dist/core/cursor.js +14 -0
- package/dist/core/cursor.js.map +1 -1
- package/dist/core/file-uri.d.ts +12 -3
- package/dist/core/file-uri.d.ts.map +1 -1
- package/dist/core/file-uri.js +35 -7
- package/dist/core/file-uri.js.map +1 -1
- package/dist/core/fmt.d.ts +2 -0
- package/dist/core/fmt.d.ts.map +1 -1
- package/dist/core/fmt.js +16 -0
- package/dist/core/fmt.js.map +1 -1
- package/dist/core/fs.d.ts +16 -1
- package/dist/core/fs.d.ts.map +1 -1
- package/dist/core/fs.js +191 -72
- package/dist/core/fs.js.map +1 -1
- package/dist/core/glob.d.ts +8 -0
- package/dist/core/glob.d.ts.map +1 -1
- package/dist/core/glob.js +84 -11
- package/dist/core/glob.js.map +1 -1
- package/dist/core/input-required.d.ts +7 -0
- package/dist/core/input-required.d.ts.map +1 -1
- package/dist/core/input-required.js +13 -0
- package/dist/core/input-required.js.map +1 -1
- package/dist/core/path-completer.d.ts.map +1 -1
- package/dist/core/path-completer.js +5 -3
- package/dist/core/path-completer.js.map +1 -1
- package/dist/core/path-utils.d.ts +51 -12
- package/dist/core/path-utils.d.ts.map +1 -1
- package/dist/core/path-utils.js +69 -29
- package/dist/core/path-utils.js.map +1 -1
- package/dist/core/path.d.ts +108 -21
- package/dist/core/path.d.ts.map +1 -1
- package/dist/core/path.js +436 -83
- package/dist/core/path.js.map +1 -1
- package/dist/core/read.d.ts +13 -3
- package/dist/core/read.d.ts.map +1 -1
- package/dist/core/read.js +46 -40
- package/dist/core/read.js.map +1 -1
- package/dist/core/schema.d.ts +9 -6
- package/dist/core/schema.d.ts.map +1 -1
- package/dist/core/schema.js +22 -19
- package/dist/core/schema.js.map +1 -1
- package/dist/core/search.d.ts +6 -0
- package/dist/core/search.d.ts.map +1 -1
- package/dist/core/search.js +159 -33
- package/dist/core/search.js.map +1 -1
- package/dist/core/sensitive.d.ts.map +1 -1
- package/dist/core/sensitive.js +71 -29
- package/dist/core/sensitive.js.map +1 -1
- package/dist/core/store.d.ts +1 -0
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +3 -0
- package/dist/core/store.js.map +1 -1
- package/dist/core/util.d.ts +12 -1
- package/dist/core/util.d.ts.map +1 -1
- package/dist/core/util.js +24 -1
- package/dist/core/util.js.map +1 -1
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/resources.d.ts +1 -2
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +20 -13
- package/dist/resources.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/create.d.ts.map +1 -1
- package/dist/tools/create.js +122 -45
- package/dist/tools/create.js.map +1 -1
- package/dist/tools/define.d.ts +8 -4
- package/dist/tools/define.d.ts.map +1 -1
- package/dist/tools/define.js +36 -9
- package/dist/tools/define.js.map +1 -1
- package/dist/tools/delete.d.ts.map +1 -1
- package/dist/tools/delete.js +57 -17
- package/dist/tools/delete.js.map +1 -1
- package/dist/tools/diff.js +1 -1
- package/dist/tools/diff.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +14 -12
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/find-files.d.ts.map +1 -1
- package/dist/tools/find-files.js +20 -20
- package/dist/tools/find-files.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +1 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/list.d.ts.map +1 -1
- package/dist/tools/list.js +142 -73
- package/dist/tools/list.js.map +1 -1
- package/dist/tools/move.d.ts.map +1 -1
- package/dist/tools/move.js +81 -36
- package/dist/tools/move.js.map +1 -1
- package/dist/tools/patch.js +13 -6
- package/dist/tools/patch.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +114 -45
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/replace-text.d.ts.map +1 -1
- package/dist/tools/replace-text.js +49 -35
- package/dist/tools/replace-text.js.map +1 -1
- package/dist/tools/search-text.d.ts.map +1 -1
- package/dist/tools/search-text.js +20 -49
- package/dist/tools/search-text.js.map +1 -1
- package/dist/tools/stat.d.ts.map +1 -1
- package/dist/tools/stat.js +26 -8
- package/dist/tools/stat.js.map +1 -1
- package/dist/transport/http-policy.d.ts +3 -2
- package/dist/transport/http-policy.d.ts.map +1 -1
- package/dist/transport/http-policy.js +3 -2
- package/dist/transport/http-policy.js.map +1 -1
- package/dist/transport/http.d.ts.map +1 -1
- package/dist/transport/http.js +2 -2
- package/dist/transport/http.js.map +1 -1
- package/dist/transport/stdio.js +1 -1
- package/dist/transport/stdio.js.map +1 -1
- package/package.json +5 -5
package/dist/core/cursor.js
CHANGED
|
@@ -17,6 +17,20 @@ function pageResult(snapshotId, offset, pageSize, snapshot) {
|
|
|
17
17
|
: undefined,
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* A paged query's snapshot identity: `method` plus every arg except
|
|
22
|
+
* `pagingFields`. Result-defining args are in the key by default, so one a tool
|
|
23
|
+
* forgets to exclude fails loud as an invalid cursor instead of replaying a
|
|
24
|
+
* stale snapshot.
|
|
25
|
+
*/
|
|
26
|
+
export function pageQueryKey(method, args, pagingFields) {
|
|
27
|
+
// Sorted: an omitted optional arg the tool fills in (e.g. path) must key the
|
|
28
|
+
// same as the caller spelling it out, whatever slot the spread lands it in.
|
|
29
|
+
const kept = Object.entries(args)
|
|
30
|
+
.filter(([key]) => !pagingFields.includes(key))
|
|
31
|
+
.sort(([a], [b]) => a.localeCompare(b));
|
|
32
|
+
return JSON.stringify({ method, ...Object.fromEntries(kept) });
|
|
33
|
+
}
|
|
20
34
|
/**
|
|
21
35
|
* The one replay-or-produce branch every paged tool runs. A cursor replays a
|
|
22
36
|
* stored snapshot; without one, `produce` runs the query and its result is
|
package/dist/core/cursor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/core/cursor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAO3C,SAAS,gBAAgB,CAAC,MAAkB;IAC1C,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AACnE,CAAC;AAED,SAAS,UAAU,CACjB,UAAkB,EAClB,MAAc,EACd,QAAgB,EAChB,QAA4B;IAE5B,IAAI,MAAM,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM;QAAE,MAAM,aAAa,EAAE,CAAC;IAC3D,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ,CAAC,CAAC;IAC7D,MAAM,UAAU,GAAG,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;IACxC,OAAO;QACL,IAAI;QACJ,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,MAAM;QACN,UAAU,EACR,UAAU,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM;YAChC,CAAC,CAAC,gBAAgB,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;YACtD,CAAC,CAAC,SAAS;KAChB,CAAC;AACJ,CAAC;AA8BD;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAU,
|
|
1
|
+
{"version":3,"file":"cursor.js","sourceRoot":"","sources":["../../src/core/cursor.ts"],"names":[],"mappings":"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","sourcesContent":["import { ErrorCode, isFsError } from './errors.ts';\nimport type { PageSnapshot, PageSnapshotStore } from './store.ts';\nimport { invalidCursor } from './store.ts';\n\ninterface PageCursor {\n readonly snapshotId: string;\n readonly offset: number;\n}\n\nfunction encodePageCursor(cursor: PageCursor): string {\n return Buffer.from(JSON.stringify(cursor)).toString('base64url');\n}\n\nfunction pageResult<T, M>(\n snapshotId: string,\n offset: number,\n pageSize: number,\n snapshot: PageSnapshot<T, M>,\n): Page<T, M> {\n if (offset >= snapshot.items.length) throw invalidCursor();\n const page = snapshot.items.slice(offset, offset + pageSize);\n const nextOffset = offset + page.length;\n return {\n page,\n metadata: snapshot.metadata,\n offset,\n nextCursor:\n nextOffset < snapshot.items.length\n ? encodePageCursor({ snapshotId, offset: nextOffset })\n : undefined,\n };\n}\n\n/**\n * The full result set a paged tool produced, before slicing. `truncated` is the\n * engine's own stop state — the hard result cap or a timeout cut the set — and\n * says nothing about page size.\n */\nexport interface ProducedPage<T, M> {\n readonly items: readonly T[];\n readonly metadata: M;\n readonly truncated: boolean;\n}\n\ninterface Page<T, M> {\n readonly page: readonly T[];\n readonly metadata: M;\n readonly nextCursor: string | undefined;\n /**\n * Index of this page's first item in the full set. A tool that reports\n * progress in its text needs it: without it every page can only say how many\n * rows it holds, which is the same number on page 1 and page 9.\n */\n readonly offset: number;\n}\n\nexport interface PaginatedPage<T, M, R> extends Page<T, M> {\n /** The externalized full set. First page only, and only when incomplete. */\n readonly resource?: R;\n}\n\n/**\n * A paged query's snapshot identity: `method` plus every arg except\n * `pagingFields`. Result-defining args are in the key by default, so one a tool\n * forgets to exclude fails loud as an invalid cursor instead of replaying a\n * stale snapshot.\n */\nexport function pageQueryKey<A extends object>(\n method: string,\n args: A,\n pagingFields: readonly (keyof A & string)[],\n): string {\n // Sorted: an omitted optional arg the tool fills in (e.g. path) must key the\n // same as the caller spelling it out, whatever slot the spread lands it in.\n const kept = Object.entries(args)\n .filter(([key]) => !pagingFields.includes(key as keyof A & string))\n .sort(([a], [b]) => a.localeCompare(b));\n return JSON.stringify({ method, ...Object.fromEntries(kept) });\n}\n\n/**\n * The one replay-or-produce branch every paged tool runs. A cursor replays a\n * stored snapshot; without one, `produce` runs the query and its result is\n * snapshotted for later pages.\n *\n * The response is incomplete when the set spans more than one page or the\n * engine truncated it. Then — and only on the first page — `externalize`\n * stores the full set and its handle rides back as `resource`. Replayed pages\n * never carry it: the stored entry expires on the resource store's own TTL and\n * LRU, not this snapshot's, so replaying its URI would hand back a dead\n * pointer. Nor do they mint a new one; the set was already stored whole.\n */\nexport async function paginate<T, M, R>(params: {\n store: PageSnapshotStore;\n /** Built by `pageQueryKey`; a snapshot replays only for the key that minted it. */\n queryKey: string;\n cursor: string | undefined;\n pageSize: number;\n produce: () => Promise<ProducedPage<T, M>>;\n externalize?: ((items: readonly T[], metadata: M) => R) | undefined;\n}): Promise<PaginatedPage<T, M, R>> {\n if (params.cursor !== undefined) {\n const decoded = decodePageCursor(params.cursor);\n const snapshot = params.store.read<T, M>(decoded.snapshotId, params.queryKey);\n return pageResult(decoded.snapshotId, decoded.offset, params.pageSize, snapshot);\n }\n const produced = await params.produce();\n const { items, metadata } = produced;\n const incomplete = items.length > params.pageSize || produced.truncated;\n const first: Page<T, M> =\n items.length <= params.pageSize\n ? { page: items, metadata, nextCursor: undefined, offset: 0 }\n : pageResult(\n params.store.create({\n queryKey: params.queryKey,\n items,\n metadata,\n }),\n 0,\n params.pageSize,\n { items, metadata },\n );\n if (!incomplete || params.externalize === undefined) return first;\n try {\n return { ...first, resource: params.externalize(produced.items, produced.metadata) };\n } catch (error) {\n // The store refuses an entry over its byte cap. The page itself is\n // fine; the caller simply gets no full-set resource for this query.\n if (isFsError(error) && error.code === ErrorCode.TOO_LARGE) return first;\n throw error;\n }\n}\n\nfunction decodePageCursor(cursor: string): PageCursor {\n try {\n const text = Buffer.from(cursor, 'base64url').toString('utf-8');\n const parsed = JSON.parse(text) as { snapshotId?: unknown; offset?: unknown };\n if (\n typeof parsed.snapshotId === 'string' &&\n parsed.snapshotId.length > 0 &&\n typeof parsed.offset === 'number' &&\n Number.isInteger(parsed.offset) &&\n parsed.offset >= 0\n ) {\n return { snapshotId: parsed.snapshotId, offset: parsed.offset };\n }\n throw new Error('Invalid page cursor');\n } catch {\n throw invalidCursor();\n }\n}\n"]}
|
package/dist/core/file-uri.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export declare const FILESYSTEM_FILE_URI_TEMPLATE = "filesystem-mcp://file/{+pat
|
|
|
6
6
|
* must return, since a client expands a suggestion into the template verbatim.
|
|
7
7
|
* Unescaped, a '#' or '?' in a filename truncates the URI into a fragment or
|
|
8
8
|
* query (silently naming a different path) and a '%' makes the decode throw.
|
|
9
|
-
* Separators are restored so the value still reads as a path.
|
|
9
|
+
* Separators are restored so the value still reads as a path. A backslash is
|
|
10
|
+
* a separator only on Windows; on POSIX it is part of a name and stays `%5C`,
|
|
11
|
+
* or `a\b.txt` and `a/b.txt` would share one URI.
|
|
10
12
|
*/
|
|
11
13
|
export declare function encodeFileUriPath(validPath: string): string;
|
|
12
14
|
/** Inverse of {@link encodeFileUriPath}; `undefined` on invalid percent-encoding. */
|
|
@@ -40,9 +42,15 @@ export interface WrittenFileMeta {
|
|
|
40
42
|
* the store serves the URI via readRaw, which would reject it with TOO_LARGE.
|
|
41
43
|
* File resources are read directly from disk, not from the result store.
|
|
42
44
|
* `lastModified` is the post-write mtime when the caller stat'd the file; a
|
|
43
|
-
* link without it is still valid.
|
|
45
|
+
* link without it is still valid. None over stdio when the read would refuse
|
|
46
|
+
* it: the JSON-encoded result past `MAX_OUTBOUND_RESULT_BYTES` (escaping
|
|
47
|
+
* counts, binary is base64), or, with no content to measure, any file whose
|
|
48
|
+
* worst-case escape could be.
|
|
44
49
|
*/
|
|
45
|
-
export declare function writtenFileLinks(validPath: string, mimeType: string, size: number | undefined, dryRun?: boolean, lastModified?: string
|
|
50
|
+
export declare function writtenFileLinks(validPath: string, mimeType: string, size: number | undefined, dryRun?: boolean, lastModified?: string, transport?: 'stdio' | 'http', written?: {
|
|
51
|
+
content: string;
|
|
52
|
+
kind: FileKind;
|
|
53
|
+
}): Pick<WrittenFileMeta, 'resourceUri' | 'resourceLink'>;
|
|
46
54
|
/**
|
|
47
55
|
* The block every write tool reports for the content it wrote (or, with
|
|
48
56
|
* `dryRun`, would have written): size, line count, MIME, and whatever
|
|
@@ -54,5 +62,6 @@ export declare function buildWrittenFileMeta(options: {
|
|
|
54
62
|
content: string;
|
|
55
63
|
dryRun?: boolean | undefined;
|
|
56
64
|
lastModified?: string | undefined;
|
|
65
|
+
transport?: 'stdio' | 'http' | undefined;
|
|
57
66
|
}): WrittenFileMeta;
|
|
58
67
|
//# sourceMappingURL=file-uri.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-uri.d.ts","sourceRoot":"","sources":["../../src/core/file-uri.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAKjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"file-uri.d.ts","sourceRoot":"","sources":["../../src/core/file-uri.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAKjE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAW1C,eAAO,MAAM,4BAA4B,kCAAkC,CAAC;AAE5E;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAG3D;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMrE;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE9D;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM;AACZ,0EAA0E;AAC1E,YAAY,CAAC,EAAE,MAAM,GACpB,YAAY,CAYd;AAiBD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAI3D;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,QAAQ,CAAC;IACf,+EAA+E;IAC/E,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,+EAA+E;IAC/E,YAAY,EAAE,YAAY,GAAG,SAAS,CAAC;CACxC;AAyBD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,MAAM,UAAQ,EACd,YAAY,CAAC,EAAE,MAAM,EACrB,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,EAC5B,OAAO,CAAC,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,GAC5C,IAAI,CAAC,eAAe,EAAE,aAAa,GAAG,cAAc,CAAC,CAavD;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE;IAC5C,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,SAAS,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;CAC1C,GAAG,eAAe,CAmBlB"}
|
package/dist/core/file-uri.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
2
|
import { detectMimeFromContent } from "./mime.js";
|
|
3
|
+
import { IS_WINDOWS, toPosixPath } from "./path-utils.js";
|
|
3
4
|
import { countLines } from "./read.js";
|
|
4
|
-
import { getMaxTextFileSize } from "./util.js";
|
|
5
|
+
import { getMaxTextFileSize, jsonBytesOver, MAX_OUTBOUND_RESULT_BYTES } from "./util.js";
|
|
6
|
+
const KIB = 1024;
|
|
5
7
|
// Single owner of the `filesystem-mcp://file/` URI scheme — the template string,
|
|
6
8
|
// the path→URI encoder, the URI→path decoder, the link blocks built from them,
|
|
7
9
|
// and the post-write metadata block every write tool reports alongside them.
|
|
@@ -11,10 +13,12 @@ export const FILESYSTEM_FILE_URI_TEMPLATE = 'filesystem-mcp://file/{+path}';
|
|
|
11
13
|
* must return, since a client expands a suggestion into the template verbatim.
|
|
12
14
|
* Unescaped, a '#' or '?' in a filename truncates the URI into a fragment or
|
|
13
15
|
* query (silently naming a different path) and a '%' makes the decode throw.
|
|
14
|
-
* Separators are restored so the value still reads as a path.
|
|
16
|
+
* Separators are restored so the value still reads as a path. A backslash is
|
|
17
|
+
* a separator only on Windows; on POSIX it is part of a name and stays `%5C`,
|
|
18
|
+
* or `a\b.txt` and `a/b.txt` would share one URI.
|
|
15
19
|
*/
|
|
16
20
|
export function encodeFileUriPath(validPath) {
|
|
17
|
-
const posix = validPath
|
|
21
|
+
const posix = IS_WINDOWS ? toPosixPath(validPath) : validPath;
|
|
18
22
|
return encodeURIComponent(posix).replace(/%2F/gi, '/');
|
|
19
23
|
}
|
|
20
24
|
/** Inverse of {@link encodeFileUriPath}; `undefined` on invalid percent-encoding. */
|
|
@@ -60,6 +64,24 @@ export function extractPath(uri) {
|
|
|
60
64
|
return undefined;
|
|
61
65
|
return decodeFileUriPath(url.pathname.slice(1));
|
|
62
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether stdio's resources/read would refuse this file: it measures the
|
|
69
|
+
* JSON-encoded result, so escaping counts, and a file it reads as binary
|
|
70
|
+
* comes back base64. Without the content, assume the worst escape (6 bytes
|
|
71
|
+
* per byte) so a link is never one the read refuses.
|
|
72
|
+
*/
|
|
73
|
+
function overStdioRead(validPath, mimeType, size, written) {
|
|
74
|
+
if (written === undefined)
|
|
75
|
+
return 6 * size > MAX_OUTBOUND_RESULT_BYTES - 64 * KIB;
|
|
76
|
+
// readRaw returns any non-text kind as base64 (`isBinary: kind !== 'text'`).
|
|
77
|
+
if (written.kind !== 'text') {
|
|
78
|
+
return 4 * Math.ceil(size / 3) > MAX_OUTBOUND_RESULT_BYTES - 64 * KIB;
|
|
79
|
+
}
|
|
80
|
+
const result = {
|
|
81
|
+
contents: [{ uri: buildFileResourceUri(validPath), mimeType, text: written.content }],
|
|
82
|
+
};
|
|
83
|
+
return jsonBytesOver(result, MAX_OUTBOUND_RESULT_BYTES) !== undefined;
|
|
84
|
+
}
|
|
63
85
|
/**
|
|
64
86
|
* The URI and link a write tool may advertise for the file it wrote. None on a
|
|
65
87
|
* dry run — nothing was written, so the file on disk is still the one the
|
|
@@ -67,10 +89,16 @@ export function extractPath(uri) {
|
|
|
67
89
|
* the store serves the URI via readRaw, which would reject it with TOO_LARGE.
|
|
68
90
|
* File resources are read directly from disk, not from the result store.
|
|
69
91
|
* `lastModified` is the post-write mtime when the caller stat'd the file; a
|
|
70
|
-
* link without it is still valid.
|
|
92
|
+
* link without it is still valid. None over stdio when the read would refuse
|
|
93
|
+
* it: the JSON-encoded result past `MAX_OUTBOUND_RESULT_BYTES` (escaping
|
|
94
|
+
* counts, binary is base64), or, with no content to measure, any file whose
|
|
95
|
+
* worst-case escape could be.
|
|
71
96
|
*/
|
|
72
|
-
export function writtenFileLinks(validPath, mimeType, size, dryRun = false, lastModified) {
|
|
73
|
-
if (dryRun ||
|
|
97
|
+
export function writtenFileLinks(validPath, mimeType, size, dryRun = false, lastModified, transport, written) {
|
|
98
|
+
if (dryRun ||
|
|
99
|
+
size === undefined ||
|
|
100
|
+
size > getMaxTextFileSize() ||
|
|
101
|
+
(transport === 'stdio' && overStdioRead(validPath, mimeType, size, written))) {
|
|
74
102
|
return { resourceUri: undefined, resourceLink: undefined };
|
|
75
103
|
}
|
|
76
104
|
return {
|
|
@@ -93,7 +121,7 @@ export function buildWrittenFileMeta(options) {
|
|
|
93
121
|
lineCount: countLines(content),
|
|
94
122
|
mimeType,
|
|
95
123
|
kind,
|
|
96
|
-
...writtenFileLinks(validPath, mimeType, size, options.dryRun, options.lastModified),
|
|
124
|
+
...writtenFileLinks(validPath, mimeType, size, options.dryRun, options.lastModified, options.transport, { content, kind }),
|
|
97
125
|
};
|
|
98
126
|
}
|
|
99
127
|
//# sourceMappingURL=file-uri.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-uri.js","sourceRoot":"","sources":["../../src/core/file-uri.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAErC,OAAO,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AAElD,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE/C,iFAAiF;AACjF,+EAA+E;AAC/E,6EAA6E;AAE7E,MAAM,CAAC,MAAM,4BAA4B,GAAG,+BAA+B,CAAC;AAE5E;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAAiB;IACjD,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IAC5C,OAAO,kBAAkB,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AACzD,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,iBAAiB,CAAC,OAAe;IAC/C,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,OAAO,yBAAyB,iBAAiB,CAAC,SAAS,CAAC,EAAE,CAAC;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACtC,GAAW,EACX,IAAY,EACZ,QAAgB,EAChB,IAAY;AACZ,0EAA0E;AAC1E,YAAqB;IAErB,OAAO;QACL,IAAI,EAAE,eAAe;QACrB,GAAG;QACH,IAAI;QACJ,QAAQ;QACR,IAAI;QACJ,WAAW,EAAE;YACX,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,CAAC;YAC/B,GAAG,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACxD;KACF,CAAC;AACJ,CAAC;AAED,SAAS,qBAAqB,CAC5B,SAAiB,EACjB,QAAgB,EAChB,IAAY,EACZ,YAAqB;IAErB,OAAO,wBAAwB,CAC7B,oBAAoB,CAAC,SAAS,CAAC,EAC/B,QAAQ,CAAC,SAAS,CAAC,EACnB,QAAQ,EACR,IAAI,EACJ,YAAY,CACb,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAC3B,IAAI,GAAG,EAAE,QAAQ,KAAK,iBAAiB,IAAI,GAAG,CAAC,IAAI,KAAK,MAAM;QAAE,OAAO,SAAS,CAAC;IACjF,OAAO,iBAAiB,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAClD,CAAC;AAaD;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAC9B,SAAiB,EACjB,QAAgB,EAChB,IAAwB,EACxB,MAAM,GAAG,KAAK,EACd,YAAqB;IAErB,IAAI,MAAM,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,GAAG,kBAAkB,EAAE,EAAE,CAAC;QAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;IAC7D,CAAC;IACD,OAAO;QACL,WAAW,EAAE,oBAAoB,CAAC,SAAS,CAAC;QAC5C,YAAY,EAAE,qBAAqB,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,CAAC;KAC7E,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAAC,OAKpC;IACC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;IACvC,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACjD,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,GAAG,qBAAqB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACrE,OAAO;QACL,IAAI;QACJ,SAAS,EAAE,UAAU,CAAC,OAAO,CAAC;QAC9B,QAAQ;QACR,IAAI;QACJ,GAAG,gBAAgB,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC;KACrF,CAAC;AACJ,CAAC","sourcesContent":["import type { ContentBlock } from '@modelcontextprotocol/server';\n\nimport { basename } from 'node:path';\n\nimport { detectMimeFromContent } from './mime.ts';\nimport type { FileKind } from './mime.ts';\nimport { countLines } from './read.ts';\nimport { getMaxTextFileSize } from './util.ts';\n\n// Single owner of the `filesystem-mcp://file/` URI scheme — the template string,\n// the path→URI encoder, the URI→path decoder, the link blocks built from them,\n// and the post-write metadata block every write tool reports alongside them.\n\nexport const FILESYSTEM_FILE_URI_TEMPLATE = 'filesystem-mcp://file/{+path}';\n\n/**\n * The `{+path}` template-variable form of a path — what `completion/complete`\n * must return, since a client expands a suggestion into the template verbatim.\n * Unescaped, a '#' or '?' in a filename truncates the URI into a fragment or\n * query (silently naming a different path) and a '%' makes the decode throw.\n * Separators are restored so the value still reads as a path.\n */\nexport function encodeFileUriPath(validPath: string): string {\n const posix = validPath.replace(/\\\\/g, '/');\n return encodeURIComponent(posix).replace(/%2F/gi, '/');\n}\n\n/** Inverse of {@link encodeFileUriPath}; `undefined` on invalid percent-encoding. */\nexport function decodeFileUriPath(encoded: string): string | undefined {\n try {\n return decodeURIComponent(encoded);\n } catch {\n return undefined;\n }\n}\n\nexport function buildFileResourceUri(validPath: string): string {\n return `filesystem-mcp://file/${encodeFileUriPath(validPath)}`;\n}\n\n/**\n * The link block for a URI already built by `buildFileResourceUri`. Callers that\n * hold the URI must use this rather than rebuilding from a path: a path that\n * differs only in case (the drive letter, on Windows) yields a *different* URI\n * string, and watchers key on that string — a link and a `resourceUri` that\n * disagree hand the client two subscriptions for one file.\n */\nexport function buildFileResourceLinkFor(\n uri: string,\n name: string,\n mimeType: string,\n size: number,\n /** ISO-8601 mtime of the file the link names, when the caller has one. */\n lastModified?: string,\n): ContentBlock {\n return {\n type: 'resource_link',\n uri,\n name,\n mimeType,\n size,\n annotations: {\n audience: ['user', 'assistant'],\n ...(lastModified !== undefined ? { lastModified } : {}),\n },\n };\n}\n\nfunction buildFileResourceLink(\n validPath: string,\n mimeType: string,\n size: number,\n lastModified?: string,\n): ContentBlock {\n return buildFileResourceLinkFor(\n buildFileResourceUri(validPath),\n basename(validPath),\n mimeType,\n size,\n lastModified,\n );\n}\n\nexport function extractPath(uri: string): string | undefined {\n const url = URL.parse(uri);\n if (url?.protocol !== 'filesystem-mcp:' || url.host !== 'file') return undefined;\n return decodeFileUriPath(url.pathname.slice(1));\n}\n\nexport interface WrittenFileMeta {\n size: number;\n lineCount: number;\n mimeType: string;\n kind: FileKind;\n /** Undefined unless {@link writtenFileLinks} allows it: see there for when. */\n resourceUri: string | undefined;\n /** Undefined unless {@link writtenFileLinks} allows it: see there for when. */\n resourceLink: ContentBlock | undefined;\n}\n\n/**\n * The URI and link a write tool may advertise for the file it wrote. None on a\n * dry run — nothing was written, so the file on disk is still the one the\n * caller already has. None when the size is unknown or over the text-size cap —\n * the store serves the URI via readRaw, which would reject it with TOO_LARGE.\n * File resources are read directly from disk, not from the result store.\n * `lastModified` is the post-write mtime when the caller stat'd the file; a\n * link without it is still valid.\n */\nexport function writtenFileLinks(\n validPath: string,\n mimeType: string,\n size: number | undefined,\n dryRun = false,\n lastModified?: string,\n): Pick<WrittenFileMeta, 'resourceUri' | 'resourceLink'> {\n if (dryRun || size === undefined || size > getMaxTextFileSize()) {\n return { resourceUri: undefined, resourceLink: undefined };\n }\n return {\n resourceUri: buildFileResourceUri(validPath),\n resourceLink: buildFileResourceLink(validPath, mimeType, size, lastModified),\n };\n}\n\n/**\n * The block every write tool reports for the content it wrote (or, with\n * `dryRun`, would have written): size, line count, MIME, and whatever\n * {@link writtenFileLinks} allows it to advertise. Named fields, because\n * `validPath` and `content` are both strings and a positional swap type-checks.\n */\nexport function buildWrittenFileMeta(options: {\n validPath: string;\n content: string;\n dryRun?: boolean | undefined;\n lastModified?: string | undefined;\n}): WrittenFileMeta {\n const { validPath, content } = options;\n const size = Buffer.byteLength(content, 'utf-8');\n const { mimeType, kind } = detectMimeFromContent(validPath, content);\n return {\n size,\n lineCount: countLines(content),\n mimeType,\n kind,\n ...writtenFileLinks(validPath, mimeType, size, options.dryRun, options.lastModified),\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"file-uri.js","sourceRoot":"","sources":["../../src/core/file-uri.ts"],"names":[],"mappings":"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","sourcesContent":["import type { ContentBlock } from '@modelcontextprotocol/server';\n\nimport { basename } from 'node:path';\n\nimport { detectMimeFromContent } from './mime.ts';\nimport type { FileKind } from './mime.ts';\nimport { IS_WINDOWS, toPosixPath } from './path-utils.ts';\nimport { countLines } from './read.ts';\nimport { getMaxTextFileSize, jsonBytesOver, MAX_OUTBOUND_RESULT_BYTES } from './util.ts';\n\nconst KIB = 1024;\n\n// Single owner of the `filesystem-mcp://file/` URI scheme — the template string,\n// the path→URI encoder, the URI→path decoder, the link blocks built from them,\n// and the post-write metadata block every write tool reports alongside them.\n\nexport const FILESYSTEM_FILE_URI_TEMPLATE = 'filesystem-mcp://file/{+path}';\n\n/**\n * The `{+path}` template-variable form of a path — what `completion/complete`\n * must return, since a client expands a suggestion into the template verbatim.\n * Unescaped, a '#' or '?' in a filename truncates the URI into a fragment or\n * query (silently naming a different path) and a '%' makes the decode throw.\n * Separators are restored so the value still reads as a path. A backslash is\n * a separator only on Windows; on POSIX it is part of a name and stays `%5C`,\n * or `a\\b.txt` and `a/b.txt` would share one URI.\n */\nexport function encodeFileUriPath(validPath: string): string {\n const posix = IS_WINDOWS ? toPosixPath(validPath) : validPath;\n return encodeURIComponent(posix).replace(/%2F/gi, '/');\n}\n\n/** Inverse of {@link encodeFileUriPath}; `undefined` on invalid percent-encoding. */\nexport function decodeFileUriPath(encoded: string): string | undefined {\n try {\n return decodeURIComponent(encoded);\n } catch {\n return undefined;\n }\n}\n\nexport function buildFileResourceUri(validPath: string): string {\n return `filesystem-mcp://file/${encodeFileUriPath(validPath)}`;\n}\n\n/**\n * The link block for a URI already built by `buildFileResourceUri`. Callers that\n * hold the URI must use this rather than rebuilding from a path: a path that\n * differs only in case (the drive letter, on Windows) yields a *different* URI\n * string, and watchers key on that string — a link and a `resourceUri` that\n * disagree hand the client two subscriptions for one file.\n */\nexport function buildFileResourceLinkFor(\n uri: string,\n name: string,\n mimeType: string,\n size: number,\n /** ISO-8601 mtime of the file the link names, when the caller has one. */\n lastModified?: string,\n): ContentBlock {\n return {\n type: 'resource_link',\n uri,\n name,\n mimeType,\n size,\n annotations: {\n audience: ['user', 'assistant'],\n ...(lastModified !== undefined ? { lastModified } : {}),\n },\n };\n}\n\nfunction buildFileResourceLink(\n validPath: string,\n mimeType: string,\n size: number,\n lastModified?: string,\n): ContentBlock {\n return buildFileResourceLinkFor(\n buildFileResourceUri(validPath),\n basename(validPath),\n mimeType,\n size,\n lastModified,\n );\n}\n\nexport function extractPath(uri: string): string | undefined {\n const url = URL.parse(uri);\n if (url?.protocol !== 'filesystem-mcp:' || url.host !== 'file') return undefined;\n return decodeFileUriPath(url.pathname.slice(1));\n}\n\nexport interface WrittenFileMeta {\n size: number;\n lineCount: number;\n mimeType: string;\n kind: FileKind;\n /** Undefined unless {@link writtenFileLinks} allows it: see there for when. */\n resourceUri: string | undefined;\n /** Undefined unless {@link writtenFileLinks} allows it: see there for when. */\n resourceLink: ContentBlock | undefined;\n}\n\n/**\n * Whether stdio's resources/read would refuse this file: it measures the\n * JSON-encoded result, so escaping counts, and a file it reads as binary\n * comes back base64. Without the content, assume the worst escape (6 bytes\n * per byte) so a link is never one the read refuses.\n */\nfunction overStdioRead(\n validPath: string,\n mimeType: string,\n size: number,\n written: { content: string; kind: FileKind } | undefined,\n): boolean {\n if (written === undefined) return 6 * size > MAX_OUTBOUND_RESULT_BYTES - 64 * KIB;\n // readRaw returns any non-text kind as base64 (`isBinary: kind !== 'text'`).\n if (written.kind !== 'text') {\n return 4 * Math.ceil(size / 3) > MAX_OUTBOUND_RESULT_BYTES - 64 * KIB;\n }\n const result = {\n contents: [{ uri: buildFileResourceUri(validPath), mimeType, text: written.content }],\n };\n return jsonBytesOver(result, MAX_OUTBOUND_RESULT_BYTES) !== undefined;\n}\n\n/**\n * The URI and link a write tool may advertise for the file it wrote. None on a\n * dry run — nothing was written, so the file on disk is still the one the\n * caller already has. None when the size is unknown or over the text-size cap —\n * the store serves the URI via readRaw, which would reject it with TOO_LARGE.\n * File resources are read directly from disk, not from the result store.\n * `lastModified` is the post-write mtime when the caller stat'd the file; a\n * link without it is still valid. None over stdio when the read would refuse\n * it: the JSON-encoded result past `MAX_OUTBOUND_RESULT_BYTES` (escaping\n * counts, binary is base64), or, with no content to measure, any file whose\n * worst-case escape could be.\n */\nexport function writtenFileLinks(\n validPath: string,\n mimeType: string,\n size: number | undefined,\n dryRun = false,\n lastModified?: string,\n transport?: 'stdio' | 'http',\n written?: { content: string; kind: FileKind },\n): Pick<WrittenFileMeta, 'resourceUri' | 'resourceLink'> {\n if (\n dryRun ||\n size === undefined ||\n size > getMaxTextFileSize() ||\n (transport === 'stdio' && overStdioRead(validPath, mimeType, size, written))\n ) {\n return { resourceUri: undefined, resourceLink: undefined };\n }\n return {\n resourceUri: buildFileResourceUri(validPath),\n resourceLink: buildFileResourceLink(validPath, mimeType, size, lastModified),\n };\n}\n\n/**\n * The block every write tool reports for the content it wrote (or, with\n * `dryRun`, would have written): size, line count, MIME, and whatever\n * {@link writtenFileLinks} allows it to advertise. Named fields, because\n * `validPath` and `content` are both strings and a positional swap type-checks.\n */\nexport function buildWrittenFileMeta(options: {\n validPath: string;\n content: string;\n dryRun?: boolean | undefined;\n lastModified?: string | undefined;\n transport?: 'stdio' | 'http' | undefined;\n}): WrittenFileMeta {\n const { validPath, content } = options;\n const size = Buffer.byteLength(content, 'utf-8');\n const { mimeType, kind } = detectMimeFromContent(validPath, content);\n return {\n size,\n lineCount: countLines(content),\n mimeType,\n kind,\n ...writtenFileLinks(\n validPath,\n mimeType,\n size,\n options.dryRun,\n options.lastModified,\n options.transport,\n { content, kind },\n ),\n };\n}\n"]}
|
package/dist/core/fmt.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface ProgressCtx {
|
|
|
10
10
|
type Phase = 'tick' | 'done' | 'fail';
|
|
11
11
|
export declare function plainMessage(phase: Phase, ctx: ProgressCtx): string;
|
|
12
12
|
export declare function formatCount(count: number, singular: string, plural?: string): string;
|
|
13
|
+
/** Human-readable byte size for tree lines and summary text: `5 B`, `1 KiB`, `1.5 KiB`, `20 KiB`. */
|
|
14
|
+
export declare function formatByteSize(bytes: number): string;
|
|
13
15
|
/**
|
|
14
16
|
* Short label for a path in summary text. `basename` returns '' for a root
|
|
15
17
|
* (`C:\`, `/`) and for a trailing-slash path, which rendered as a bare
|
package/dist/core/fmt.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fmt.d.ts","sourceRoot":"","sources":["../../src/core/fmt.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAkCtC,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM,CAGnE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAiB,GAAG,MAAM,CAE5F;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAKD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,SAAQ,GAAG,MAAM,CAI9E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/D;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC/C,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C,GAAG,MAAM,CAsBT;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAG9D;AASD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;CACrB,GAAG,MAAM,GAAG,SAAS,CAWrB"}
|
|
1
|
+
{"version":3,"file":"fmt.d.ts","sourceRoot":"","sources":["../../src/core/fmt.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAkCtC,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,GAAG,MAAM,CAGnE;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAiB,GAAG,MAAM,CAE5F;AAID,qGAAqG;AACrG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAapD;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAKD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,SAAS,SAAQ,GAAG,MAAM,CAI9E;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAE/D;AAYD;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC/C,aAAa,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACnC,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,mBAAmB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC1C,GAAG,MAAM,CAsBT;AAED,iDAAiD;AACjD,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAG9D;AASD;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;CACrB,GAAG,MAAM,GAAG,SAAS,CAWrB"}
|
package/dist/core/fmt.js
CHANGED
|
@@ -37,6 +37,22 @@ export function plainMessage(phase, ctx) {
|
|
|
37
37
|
export function formatCount(count, singular, plural = `${singular}s`) {
|
|
38
38
|
return `${String(count)} ${count === 1 ? singular : plural}`;
|
|
39
39
|
}
|
|
40
|
+
const SIZE_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];
|
|
41
|
+
/** Human-readable byte size for tree lines and summary text: `5 B`, `1 KiB`, `1.5 KiB`, `20 KiB`. */
|
|
42
|
+
export function formatByteSize(bytes) {
|
|
43
|
+
let value = bytes;
|
|
44
|
+
let unit = 0;
|
|
45
|
+
while (value >= 1024 && unit < SIZE_UNITS.length - 1) {
|
|
46
|
+
value /= 1024;
|
|
47
|
+
unit++;
|
|
48
|
+
}
|
|
49
|
+
let rounded = unit === 0 || value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
50
|
+
if (rounded >= 1024 && unit < SIZE_UNITS.length - 1) {
|
|
51
|
+
rounded = 1;
|
|
52
|
+
unit++;
|
|
53
|
+
}
|
|
54
|
+
return `${String(rounded)} ${SIZE_UNITS[unit]}`;
|
|
55
|
+
}
|
|
40
56
|
/**
|
|
41
57
|
* Short label for a path in summary text. `basename` returns '' for a root
|
|
42
58
|
* (`C:\`, `/`) and for a trailing-slash path, which rendered as a bare
|
package/dist/core/fmt.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fmt.js","sourceRoot":"","sources":["../../src/core/fmt.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename } from 'node:path';\n\nimport { MAX_SEARCH_RESULTS } from './util.ts';\n\nexport interface ProgressCtx {\n label: string;\n subject?: string;\n scope?: string;\n current?: number;\n total?: number;\n detail?: string;\n error?: string;\n}\n\ntype Phase = 'tick' | 'done' | 'fail';\n\nfunction buildBody(ctx: ProgressCtx, phase: Phase): string {\n const items: string[] = [];\n if (ctx.subject) {\n items.push(ctx.subject);\n }\n\n switch (phase) {\n case 'tick':\n if (ctx.current !== undefined && ctx.total !== undefined) {\n items.push(`${ctx.current}/${ctx.total}`);\n } else if (ctx.current !== undefined) {\n items.push(String(ctx.current));\n }\n break;\n case 'done':\n if (ctx.scope) {\n items.push(ctx.scope);\n }\n if (ctx.detail) {\n items.push(ctx.detail);\n }\n break;\n case 'fail':\n if (ctx.error) {\n items.push(ctx.error);\n }\n break;\n }\n\n return items.join(' · ');\n}\n\nexport function plainMessage(phase: Phase, ctx: ProgressCtx): string {\n const body = buildBody(ctx, phase);\n return body ? `${ctx.label}: ${body}` : `${ctx.label}:`;\n}\n\nexport function formatCount(count: number, singular: string, plural = `${singular}s`): string {\n return `${String(count)} ${count === 1 ? singular : plural}`;\n}\n\n/**\n * Short label for a path in summary text. `basename` returns '' for a root\n * (`C:\\`, `/`) and for a trailing-slash path, which rendered as a bare\n * separator in the per-path summaries, so fall back to the full path.\n */\nexport function pathLabel(path: string): string {\n return basename(path) || path;\n}\n\n/** Names past this point are noise in a summary line; structuredContent holds them all. */\nconst MAX_ROSTER_ITEMS = 20;\n\n/**\n * Join a per-path summary roster, capped. A 1000-path delete would otherwise\n * name every entry in the text block that `structuredContent` already carries\n * in full.\n */\nexport function joinRoster(items: readonly string[], separator = ' · '): string {\n if (items.length <= MAX_ROSTER_ITEMS) return items.join(separator);\n const shown = items.slice(0, MAX_ROSTER_ITEMS).join(separator);\n return `${shown}${separator}+${String(items.length - MAX_ROSTER_ITEMS)} more`;\n}\n\nexport function truncateProgressPattern(pattern: string): string {\n return pattern.length <= 40 ? pattern : `${pattern.slice(0, 40)}…`;\n}\n\n/**\n * `stoppedReason` names the engine's own cap, and `maxResults` is also the name\n * of the caller's page-size argument — spelling the cap out keeps a caller who\n * passed `maxResults: 5` from reading their own argument back.\n */\nconst STOP_REASON_TEXT: Record<string, string> = {\n maxResults: `hit the server's ${String(MAX_SEARCH_RESULTS)}-result scan cap, not your maxResults`,\n timeout: 'hit the time limit, or the request was cancelled',\n};\n\n/**\n * The `//` lines a paged search appends to its text block. Paging, the\n * engine's stop state and the files it skipped live in the structured half,\n * which `defineTool` ships under `_meta` for a tool that authors its own text —\n * and no client renders that. These lines are the only place a caller learns\n * that more remains, that the scan was cut, or that files went unexamined (a\n * lone match in an oversized file otherwise reads as \"No matches\"). They are\n * independent: a scan can stop early with too few results to page, so a\n * truncation with no cursor must still say so. Prefixed `//` so none can be\n * mistaken for a result row.\n */\nexport function pageTrailer(p: {\n offset: number;\n shown: number;\n total: number;\n noun: string;\n tool: string;\n nextCursor?: string | undefined;\n /** The call's own arguments; printed merged with the cursor so the hint is sendable as-is. */\n nextArgs?: Record<string, unknown> | undefined;\n stoppedReason?: string | undefined;\n skippedTooLarge?: number | undefined;\n skippedInaccessible?: number | undefined;\n}): string {\n const lines: string[] = [];\n // Position is owed on every page of a split set, including the last one —\n // which has no cursor and would otherwise read as the whole answer.\n if (p.offset > 0 || p.total > p.shown) {\n const next =\n p.nextCursor === undefined\n ? ''\n : ` Next page: ${p.tool} ${JSON.stringify({ ...p.nextArgs, cursor: p.nextCursor })}`;\n lines.push(\n `// showing ${String(p.offset + 1)}-${String(p.offset + p.shown)} of ${String(p.total)} ${p.noun}.${next}`,\n );\n }\n if (p.stoppedReason !== undefined) lines.push(stoppedEarlyLine(p.stoppedReason));\n const skipped = [\n p.skippedTooLarge ? `${formatCount(p.skippedTooLarge, 'file')} over the text size limit` : '',\n p.skippedInaccessible ? formatCount(p.skippedInaccessible, 'inaccessible file') : '',\n ].filter(Boolean);\n if (skipped.length > 0) {\n lines.push(`// skipped ${skipped.join(', ')}; results may be incomplete.`);\n }\n return lines.length > 0 ? `\\n\\n${lines.join('\\n')}` : '';\n}\n\n/** The `//` line saying a scan was cut short. */\nexport function stoppedEarlyLine(stoppedReason: string): string {\n const why = STOP_REASON_TEXT[stoppedReason] ?? `stopped (${stoppedReason})`;\n return `// scan stopped early: ${why}. That total is a floor, not the count. Narrow path or pattern for the rest.`;\n}\n\n// Regex syntax literal code rarely holds: unspaced alternation (`a|b`, not\n// `||` or a `string | null` union), class escapes, and `.*` / `.+`. Bare `(`,\n// `.`, `[` stay out — `foo(` and `a.b` are ordinary literal needles. A Windows\n// path like `src\\db` can still trip `\\d`; the hint only shows on zero\n// matches, so a stray one costs one line.\nconst REGEX_META = /(?<=[^\\s|])\\|(?=[^\\s|])|\\\\[dDwWsSbB]|\\.[*+]/;\n\n/**\n * The `//` line explaining a zero-match result, or undefined. Without it a\n * regex matched literally, or a glob that selected no files, reads exactly\n * like a genuine miss (issue #55). An `incomplete` scan (files skipped or\n * failed, scan stopped) already has its own line, which explains the zero\n * better, so it gets no hint.\n */\nexport function zeroMatchHint(p: {\n searchPattern: string;\n isRegex: boolean;\n pattern?: string | undefined;\n filesScanned: number;\n incomplete: boolean;\n}): string | undefined {\n if (p.incomplete) return undefined;\n if (p.filesScanned === 0) {\n return p.pattern === undefined\n ? '// no files were searched: nothing under path passed the hidden/ignored/maxDepth filters.'\n : `// no files were searched: nothing under path matched pattern '${p.pattern}' after the hidden/ignored/maxDepth filters. pattern is a file-name glob; the text to find goes in searchPattern.`;\n }\n if (!p.isRegex && REGEX_META.test(p.searchPattern)) {\n return '// searchPattern was matched as literal text but looks like a regex; pass isRegex=true to match it as one.';\n }\n return undefined;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"fmt.js","sourceRoot":"","sources":["../../src/core/fmt.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename } from 'node:path';\n\nimport { MAX_SEARCH_RESULTS } from './util.ts';\n\nexport interface ProgressCtx {\n label: string;\n subject?: string;\n scope?: string;\n current?: number;\n total?: number;\n detail?: string;\n error?: string;\n}\n\ntype Phase = 'tick' | 'done' | 'fail';\n\nfunction buildBody(ctx: ProgressCtx, phase: Phase): string {\n const items: string[] = [];\n if (ctx.subject) {\n items.push(ctx.subject);\n }\n\n switch (phase) {\n case 'tick':\n if (ctx.current !== undefined && ctx.total !== undefined) {\n items.push(`${ctx.current}/${ctx.total}`);\n } else if (ctx.current !== undefined) {\n items.push(String(ctx.current));\n }\n break;\n case 'done':\n if (ctx.scope) {\n items.push(ctx.scope);\n }\n if (ctx.detail) {\n items.push(ctx.detail);\n }\n break;\n case 'fail':\n if (ctx.error) {\n items.push(ctx.error);\n }\n break;\n }\n\n return items.join(' · ');\n}\n\nexport function plainMessage(phase: Phase, ctx: ProgressCtx): string {\n const body = buildBody(ctx, phase);\n return body ? `${ctx.label}: ${body}` : `${ctx.label}:`;\n}\n\nexport function formatCount(count: number, singular: string, plural = `${singular}s`): string {\n return `${String(count)} ${count === 1 ? singular : plural}`;\n}\n\nconst SIZE_UNITS = ['B', 'KiB', 'MiB', 'GiB', 'TiB'];\n\n/** Human-readable byte size for tree lines and summary text: `5 B`, `1 KiB`, `1.5 KiB`, `20 KiB`. */\nexport function formatByteSize(bytes: number): string {\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024;\n unit++;\n }\n let rounded = unit === 0 || value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;\n if (rounded >= 1024 && unit < SIZE_UNITS.length - 1) {\n rounded = 1;\n unit++;\n }\n return `${String(rounded)} ${SIZE_UNITS[unit]}`;\n}\n\n/**\n * Short label for a path in summary text. `basename` returns '' for a root\n * (`C:\\`, `/`) and for a trailing-slash path, which rendered as a bare\n * separator in the per-path summaries, so fall back to the full path.\n */\nexport function pathLabel(path: string): string {\n return basename(path) || path;\n}\n\n/** Names past this point are noise in a summary line; structuredContent holds them all. */\nconst MAX_ROSTER_ITEMS = 20;\n\n/**\n * Join a per-path summary roster, capped. A 1000-path delete would otherwise\n * name every entry in the text block that `structuredContent` already carries\n * in full.\n */\nexport function joinRoster(items: readonly string[], separator = ' · '): string {\n if (items.length <= MAX_ROSTER_ITEMS) return items.join(separator);\n const shown = items.slice(0, MAX_ROSTER_ITEMS).join(separator);\n return `${shown}${separator}+${String(items.length - MAX_ROSTER_ITEMS)} more`;\n}\n\nexport function truncateProgressPattern(pattern: string): string {\n return pattern.length <= 40 ? pattern : `${pattern.slice(0, 40)}…`;\n}\n\n/**\n * `stoppedReason` names the engine's own cap, and `maxResults` is also the name\n * of the caller's page-size argument — spelling the cap out keeps a caller who\n * passed `maxResults: 5` from reading their own argument back.\n */\nconst STOP_REASON_TEXT: Record<string, string> = {\n maxResults: `hit the server's ${String(MAX_SEARCH_RESULTS)}-result scan cap, not your maxResults`,\n timeout: 'hit the time limit, or the request was cancelled',\n};\n\n/**\n * The `//` lines a paged search appends to its text block. Paging, the\n * engine's stop state and the files it skipped live in the structured half,\n * which `defineTool` ships under `_meta` for a tool that authors its own text —\n * and no client renders that. These lines are the only place a caller learns\n * that more remains, that the scan was cut, or that files went unexamined (a\n * lone match in an oversized file otherwise reads as \"No matches\"). They are\n * independent: a scan can stop early with too few results to page, so a\n * truncation with no cursor must still say so. Prefixed `//` so none can be\n * mistaken for a result row.\n */\nexport function pageTrailer(p: {\n offset: number;\n shown: number;\n total: number;\n noun: string;\n tool: string;\n nextCursor?: string | undefined;\n /** The call's own arguments; printed merged with the cursor so the hint is sendable as-is. */\n nextArgs?: Record<string, unknown> | undefined;\n stoppedReason?: string | undefined;\n skippedTooLarge?: number | undefined;\n skippedInaccessible?: number | undefined;\n}): string {\n const lines: string[] = [];\n // Position is owed on every page of a split set, including the last one —\n // which has no cursor and would otherwise read as the whole answer.\n if (p.offset > 0 || p.total > p.shown) {\n const next =\n p.nextCursor === undefined\n ? ''\n : ` Next page: ${p.tool} ${JSON.stringify({ ...p.nextArgs, cursor: p.nextCursor })}`;\n lines.push(\n `// showing ${String(p.offset + 1)}-${String(p.offset + p.shown)} of ${String(p.total)} ${p.noun}.${next}`,\n );\n }\n if (p.stoppedReason !== undefined) lines.push(stoppedEarlyLine(p.stoppedReason));\n const skipped = [\n p.skippedTooLarge ? `${formatCount(p.skippedTooLarge, 'file')} over the text size limit` : '',\n p.skippedInaccessible ? formatCount(p.skippedInaccessible, 'inaccessible file') : '',\n ].filter(Boolean);\n if (skipped.length > 0) {\n lines.push(`// skipped ${skipped.join(', ')}; results may be incomplete.`);\n }\n return lines.length > 0 ? `\\n\\n${lines.join('\\n')}` : '';\n}\n\n/** The `//` line saying a scan was cut short. */\nexport function stoppedEarlyLine(stoppedReason: string): string {\n const why = STOP_REASON_TEXT[stoppedReason] ?? `stopped (${stoppedReason})`;\n return `// scan stopped early: ${why}. That total is a floor, not the count. Narrow path or pattern for the rest.`;\n}\n\n// Regex syntax literal code rarely holds: unspaced alternation (`a|b`, not\n// `||` or a `string | null` union), class escapes, and `.*` / `.+`. Bare `(`,\n// `.`, `[` stay out — `foo(` and `a.b` are ordinary literal needles. A Windows\n// path like `src\\db` can still trip `\\d`; the hint only shows on zero\n// matches, so a stray one costs one line.\nconst REGEX_META = /(?<=[^\\s|])\\|(?=[^\\s|])|\\\\[dDwWsSbB]|\\.[*+]/;\n\n/**\n * The `//` line explaining a zero-match result, or undefined. Without it a\n * regex matched literally, or a glob that selected no files, reads exactly\n * like a genuine miss (issue #55). An `incomplete` scan (files skipped or\n * failed, scan stopped) already has its own line, which explains the zero\n * better, so it gets no hint.\n */\nexport function zeroMatchHint(p: {\n searchPattern: string;\n isRegex: boolean;\n pattern?: string | undefined;\n filesScanned: number;\n incomplete: boolean;\n}): string | undefined {\n if (p.incomplete) return undefined;\n if (p.filesScanned === 0) {\n return p.pattern === undefined\n ? '// no files were searched: nothing under path passed the hidden/ignored/maxDepth filters.'\n : `// no files were searched: nothing under path matched pattern '${p.pattern}' after the hidden/ignored/maxDepth filters. pattern is a file-name glob; the text to find goes in searchPattern.`;\n }\n if (!p.isRegex && REGEX_META.test(p.searchPattern)) {\n return '// searchPattern was matched as literal text but looks like a regex; pass isRegex=true to match it as one.';\n }\n return undefined;\n}\n"]}
|
package/dist/core/fs.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Stats } from 'node:fs';
|
|
2
2
|
import type { FileHandle } from 'node:fs/promises';
|
|
3
3
|
import { cp as fsCp, mkdir as fsMkdir, readlink as fsReadlink, rm as fsRm, rmdir as fsRmdir } from 'node:fs/promises';
|
|
4
|
+
import { type FileKind } from './mime.ts';
|
|
4
5
|
import type { EntryType as FileType } from './path-utils.ts';
|
|
5
6
|
import type { PathGuard } from './path.ts';
|
|
6
7
|
import type { ReadFileResult, ReadSpec } from './read.ts';
|
|
@@ -24,7 +25,7 @@ export declare class GuardedFileSystem {
|
|
|
24
25
|
validPath: string;
|
|
25
26
|
}>;
|
|
26
27
|
lstat(filePath: string, options?: {
|
|
27
|
-
signal?: AbortSignal;
|
|
28
|
+
signal?: AbortSignal | undefined;
|
|
28
29
|
}): Promise<{
|
|
29
30
|
stats: Stats;
|
|
30
31
|
validPath: string;
|
|
@@ -84,7 +85,10 @@ export declare class GuardedFileSystem {
|
|
|
84
85
|
}): Promise<{
|
|
85
86
|
content: Buffer;
|
|
86
87
|
mimeType: string;
|
|
88
|
+
kind: FileKind;
|
|
87
89
|
isBinary: boolean;
|
|
90
|
+
validPath: string;
|
|
91
|
+
modified: string;
|
|
88
92
|
}>;
|
|
89
93
|
open(filePath: string): Promise<FileHandle>;
|
|
90
94
|
statDetailed(filePath: string, options?: {
|
|
@@ -95,6 +99,17 @@ export declare class GuardedFileSystem {
|
|
|
95
99
|
stats: Stats;
|
|
96
100
|
}>;
|
|
97
101
|
hasChildrenUnchecked(dirPath: string): Promise<boolean>;
|
|
102
|
+
/**
|
|
103
|
+
* Direct children of a directory, leaving out entries the guard treats as
|
|
104
|
+
* sensitive so the count never reveals what listings hide. Hidden and
|
|
105
|
+
* ignored entries count: this answers "is it empty?", not "what would list show?".
|
|
106
|
+
* A symlinked child counts only when list would show it. The path validation, open, opendir,
|
|
107
|
+
* each read and each link check race the signal, so a stalled mount ends the call at its
|
|
108
|
+
* deadline.
|
|
109
|
+
*/
|
|
110
|
+
countEntries(dirPath: string, options?: {
|
|
111
|
+
signal?: AbortSignal;
|
|
112
|
+
}): Promise<number>;
|
|
98
113
|
}
|
|
99
114
|
/**
|
|
100
115
|
* Returns whether `path` exists, treating "not found" as absent and logging any
|
package/dist/core/fs.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/core/fs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fs.d.ts","sourceRoot":"","sources":["../../src/core/fs.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,KAAK,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAEL,EAAE,IAAI,IAAI,EAEV,KAAK,IAAI,OAAO,EAIhB,QAAQ,IAAI,UAAU,EAEtB,EAAE,IAAI,IAAI,EACV,KAAK,IAAI,OAAO,EAIjB,MAAM,kBAAkB,CAAC;AAY1B,OAAO,EAAyB,KAAK,QAAQ,EAAoB,MAAM,WAAW,CAAC;AAEnF,OAAO,KAAK,EAAE,SAAS,IAAI,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAY1D,YAAY,EAAE,QAAQ,EAAE,CAAC;AACzB,YAAY,EAAE,KAAK,EAAE,CAAC;AAetB,qBAAa,iBAAiB;;IAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;gBAElB,SAAS,EAAE,SAAS;IA8FhC;;;;;OAKG;IACG,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAKzF,IAAI,CACR,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAE,GAC7C,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAMzC,KAAK,CACT,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAE,GAC7C,OAAO,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAMzC,QAAQ,CACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC,CAAC,CAAC,GACzC,OAAO,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAU/C,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAK/E,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBvD,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAO,GACjD,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IA0DjC;;;;;OAKG;IACG,UAAU,CACd,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;QAAE,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;KAAO,GACjD,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAmEjC;;;;OAIG;IACG,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAO1D;;;;;;;;OAQG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAqBjD,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAK/E,EAAE,CACN,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC,GACnC,OAAO,CAAC,IAAI,CAAC;IAQV,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,CAAC,cAAc,CAAC;IASnE,gBAAgB,CACpB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,GAChD,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC;IA0B1D,OAAO,CACX,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,OAAO,CAAC;QACT,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,QAAQ,CAAC;QACf,QAAQ,EAAE,OAAO,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IAkCI,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAQ3C,YAAY,CAChB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GACjC,OAAO,CAAC;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC;IAOjE,oBAAoB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAc7D;;;;;;;OAOG;IACG,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC;CAqGzF;AAED;;;GAGG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,IAAI,CAAC,iBAAiB,EAAE,MAAM,CAAC,EACnC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,OAAO,CAAC,CAWlB"}
|