@j0hanz/filesystem-mcp 2.1.7 → 2.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.
Files changed (145) hide show
  1. package/README.md +8 -9
  2. package/dist/cli.d.ts +1 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +29 -34
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/concurrency.d.ts +4 -7
  7. package/dist/core/concurrency.d.ts.map +1 -1
  8. package/dist/core/concurrency.js +4 -12
  9. package/dist/core/concurrency.js.map +1 -1
  10. package/dist/core/cursor.d.ts +11 -0
  11. package/dist/core/cursor.d.ts.map +1 -1
  12. package/dist/core/cursor.js +18 -20
  13. package/dist/core/cursor.js.map +1 -1
  14. package/dist/core/fmt.d.ts +2 -1
  15. package/dist/core/fmt.d.ts.map +1 -1
  16. package/dist/core/fmt.js +8 -15
  17. package/dist/core/fmt.js.map +1 -1
  18. package/dist/core/fs.d.ts +0 -14
  19. package/dist/core/fs.d.ts.map +1 -1
  20. package/dist/core/fs.js +1 -0
  21. package/dist/core/fs.js.map +1 -1
  22. package/dist/core/glob.d.ts +4 -16
  23. package/dist/core/glob.d.ts.map +1 -1
  24. package/dist/core/glob.js +16 -33
  25. package/dist/core/glob.js.map +1 -1
  26. package/dist/core/input-required.d.ts +10 -2
  27. package/dist/core/input-required.d.ts.map +1 -1
  28. package/dist/core/input-required.js +10 -0
  29. package/dist/core/input-required.js.map +1 -1
  30. package/dist/core/observability.d.ts.map +1 -1
  31. package/dist/core/observability.js +2 -1
  32. package/dist/core/observability.js.map +1 -1
  33. package/dist/core/path-completer.d.ts.map +1 -1
  34. package/dist/core/path-completer.js +4 -27
  35. package/dist/core/path-completer.js.map +1 -1
  36. package/dist/core/path-utils.d.ts +1 -1
  37. package/dist/core/path-utils.d.ts.map +1 -1
  38. package/dist/core/path-utils.js +2 -2
  39. package/dist/core/path-utils.js.map +1 -1
  40. package/dist/core/path.d.ts +7 -9
  41. package/dist/core/path.d.ts.map +1 -1
  42. package/dist/core/path.js +24 -42
  43. package/dist/core/path.js.map +1 -1
  44. package/dist/core/primitives.d.ts +7 -0
  45. package/dist/core/primitives.d.ts.map +1 -1
  46. package/dist/core/primitives.js +15 -8
  47. package/dist/core/primitives.js.map +1 -1
  48. package/dist/core/read.d.ts.map +1 -1
  49. package/dist/core/read.js +6 -23
  50. package/dist/core/read.js.map +1 -1
  51. package/dist/core/schema.d.ts.map +1 -1
  52. package/dist/core/schema.js +19 -74
  53. package/dist/core/schema.js.map +1 -1
  54. package/dist/core/search.d.ts +4 -11
  55. package/dist/core/search.d.ts.map +1 -1
  56. package/dist/core/search.js +11 -18
  57. package/dist/core/search.js.map +1 -1
  58. package/dist/core/store.d.ts.map +1 -1
  59. package/dist/core/store.js +6 -17
  60. package/dist/core/store.js.map +1 -1
  61. package/dist/core/util.d.ts.map +1 -1
  62. package/dist/core/util.js +2 -11
  63. package/dist/core/util.js.map +1 -1
  64. package/dist/index.js +15 -42
  65. package/dist/index.js.map +1 -1
  66. package/dist/instructions.js +11 -11
  67. package/dist/instructions.js.map +1 -1
  68. package/dist/prompts.d.ts.map +1 -1
  69. package/dist/prompts.js +17 -25
  70. package/dist/prompts.js.map +1 -1
  71. package/dist/resources.d.ts +4 -4
  72. package/dist/resources.d.ts.map +1 -1
  73. package/dist/resources.js +7 -17
  74. package/dist/resources.js.map +1 -1
  75. package/dist/server.d.ts +1 -12
  76. package/dist/server.d.ts.map +1 -1
  77. package/dist/server.js +15 -30
  78. package/dist/server.js.map +1 -1
  79. package/dist/tools/define.d.ts.map +1 -1
  80. package/dist/tools/define.js +39 -65
  81. package/dist/tools/define.js.map +1 -1
  82. package/dist/tools/delete.d.ts +2 -0
  83. package/dist/tools/delete.d.ts.map +1 -0
  84. package/dist/tools/{delete-file.js → delete.js} +35 -40
  85. package/dist/tools/delete.js.map +1 -0
  86. package/dist/tools/find-files.d.ts +2 -0
  87. package/dist/tools/find-files.d.ts.map +1 -0
  88. package/dist/tools/{search-files.js → find-files.js} +14 -19
  89. package/dist/tools/find-files.js.map +1 -0
  90. package/dist/tools/index.d.ts +6 -6
  91. package/dist/tools/index.d.ts.map +1 -1
  92. package/dist/tools/index.js +15 -15
  93. package/dist/tools/index.js.map +1 -1
  94. package/dist/tools/list-roots.d.ts +2 -0
  95. package/dist/tools/list-roots.d.ts.map +1 -0
  96. package/dist/tools/{roots.js → list-roots.js} +2 -2
  97. package/dist/tools/list-roots.js.map +1 -0
  98. package/dist/tools/list.d.ts.map +1 -1
  99. package/dist/tools/list.js +29 -37
  100. package/dist/tools/list.js.map +1 -1
  101. package/dist/tools/move.d.ts.map +1 -1
  102. package/dist/tools/move.js +5 -7
  103. package/dist/tools/move.js.map +1 -1
  104. package/dist/tools/progress.d.ts +1 -1
  105. package/dist/tools/progress.d.ts.map +1 -1
  106. package/dist/tools/progress.js.map +1 -1
  107. package/dist/tools/read.d.ts +1 -1
  108. package/dist/tools/read.d.ts.map +1 -1
  109. package/dist/tools/read.js +37 -43
  110. package/dist/tools/read.js.map +1 -1
  111. package/dist/tools/replace-text.d.ts +2 -0
  112. package/dist/tools/replace-text.d.ts.map +1 -0
  113. package/dist/tools/{replace-in-files.js → replace-text.js} +9 -23
  114. package/dist/tools/replace-text.js.map +1 -0
  115. package/dist/tools/search-text.d.ts +2 -0
  116. package/dist/tools/search-text.d.ts.map +1 -0
  117. package/dist/tools/{search-content.js → search-text.js} +10 -37
  118. package/dist/tools/search-text.js.map +1 -0
  119. package/dist/tools/stat.d.ts +1 -1
  120. package/dist/tools/stat.d.ts.map +1 -1
  121. package/dist/tools/stat.js +10 -38
  122. package/dist/tools/stat.js.map +1 -1
  123. package/dist/transport/http-policy.d.ts +18 -15
  124. package/dist/transport/http-policy.d.ts.map +1 -1
  125. package/dist/transport/http-policy.js +29 -39
  126. package/dist/transport/http-policy.js.map +1 -1
  127. package/dist/transport/http.d.ts.map +1 -1
  128. package/dist/transport/http.js +29 -39
  129. package/dist/transport/http.js.map +1 -1
  130. package/package.json +4 -7
  131. package/dist/tools/delete-file.d.ts +0 -2
  132. package/dist/tools/delete-file.d.ts.map +0 -1
  133. package/dist/tools/delete-file.js.map +0 -1
  134. package/dist/tools/replace-in-files.d.ts +0 -2
  135. package/dist/tools/replace-in-files.d.ts.map +0 -1
  136. package/dist/tools/replace-in-files.js.map +0 -1
  137. package/dist/tools/roots.d.ts +0 -2
  138. package/dist/tools/roots.d.ts.map +0 -1
  139. package/dist/tools/roots.js.map +0 -1
  140. package/dist/tools/search-content.d.ts +0 -2
  141. package/dist/tools/search-content.d.ts.map +0 -1
  142. package/dist/tools/search-content.js.map +0 -1
  143. package/dist/tools/search-files.d.ts +0 -2
  144. package/dist/tools/search-files.d.ts.map +0 -1
  145. package/dist/tools/search-files.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"input-required.js","sourceRoot":"","sources":["../../src/core/input-required.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared infrastructure for the SEP-2577 `input_required` multi-round-trip\n// destructive-confirmation flows (recursive delete, move overwrite, out-of-root\n// access grant). A handler returns `inputRequired(...)` instead of the deprecated\n// push-style server-to-client elicitation request; the client retries the same\n// `tools/call` carrying `inputResponses`, and the handler re-enters from the top\n// reading the verified `requestState` and the accepted responses.\n//\n// `requestState` round-trips through the client (attacker-controlled on\n// re-entry), so it is sealed with the SDK's HMAC-SHA256 codec. The payload binds\n// each confirmation to its operation kind and sorted target-path set (spec R9,\n// R10); the handler additionally rejects any retry whose decoded paths do not\n// match the retried request's parameters (the codec only proves the state was\n// not tampered — it cannot see the current args).\nimport type {\n ClientCapabilities,\n InputRequest,\n InputRequiredResult,\n RequestStateCodec,\n} from '@modelcontextprotocol/server';\nimport {\n acceptedContent,\n createRequestStateCodec,\n inputRequired,\n} from '@modelcontextprotocol/server';\n\nimport { randomBytes } from 'node:crypto';\n\nimport * as z from 'zod/v4';\n\nimport { ErrorCode, FsError } from './errors.js';\nimport { Logger } from './observability.js';\n\n/** The destructive operation a pending confirmation authorizes. */\ntype PendingOp = 'delete' | 'move' | 'copy' | 'grant';\n\n/**\n * Integrity-protected state minted into an `input_required` result and echoed\n * back by the client on retry. `paths` is the sorted canonical set of target\n * paths the confirmation covers; the handler compares it to the retried\n * request's parameters and rejects a mismatch (R9).\n */\nexport interface PendingState {\n readonly op: PendingOp;\n readonly paths: readonly string[];\n}\n\n/** One embedded form-mode confirmation, keyed within the call. */\nexport interface PendingInput {\n /** Server-assigned key, unique within the `tools/call`. */\n readonly key: string;\n /** Human-readable prompt for this item. */\n readonly message: string;\n /**\n * When set, the form offers a titled single-select enum (`choice` field)\n * instead of a boolean `confirm`. Each entry is a `{ value, title }` pair.\n */\n readonly choices?: readonly { value: string; title: string }[];\n /**\n * When set with `choices`, the form offers a multi-select enum: `choice` is\n * a string array (`MultiSelectEnumSchema` shape) and the caller reads the\n * accepted set with `readAcceptedMultiChoice`. Single-select otherwise.\n */\n readonly multi?: boolean;\n}\n\n/**\n * HMAC key for the requestState codec. Read once from\n * `FS_REQUEST_STATE_KEY` (UTF-8, must be >=32 bytes); a random\n * 32-byte key is generated at boot when the env var is unset or too short. A\n * per-process key is correct for stdio and the single-node HTTP leg (decision\n * record 11). A server restart invalidates in-flight tokens; the client\n * re-requests, which is fail-closed and safe.\n */\nfunction configuredRequestStateKey(): Uint8Array | undefined {\n const env = process.env['FS_REQUEST_STATE_KEY'];\n if (env) {\n const bytes = Buffer.from(env, 'utf8');\n if (bytes.length >= 32) return bytes;\n Logger.warn(\n `FS_REQUEST_STATE_KEY is ${String(bytes.length)} bytes; 32 are required. Falling back to a random per-boot key — in-flight input_required rounds will not survive a restart.`,\n );\n }\n return undefined;\n}\n\n// Built on first use, not at module load, so an unset env var costs nothing at\n// import and the random per-boot fallback is minted only once something needs it.\nlet codec: RequestStateCodec<PendingState> | undefined;\n\nfunction getRequestStateCodec(): RequestStateCodec<PendingState> {\n codec ??= createRequestStateCodec<PendingState>({\n key: configuredRequestStateKey() ?? randomBytes(32),\n });\n return codec;\n}\n\n/**\n * The codec: `mint` seals a `PendingState` into the opaque wire string a\n * handler returns from `inputRequired({ requestState })`; `verify` drops into\n * `ServerOptions.requestState.verify` and throws on tamper, expiry, or bind\n * mismatch (the seam answers with the frozen `-32602`).\n */\nexport const requestStateCodec: RequestStateCodec<PendingState> = {\n mint: (payload, ctx) => getRequestStateCodec().mint(payload, ctx),\n verify: (state, ctx) => getRequestStateCodec().verify(state, ctx),\n};\n\n/**\n * Build a single-select enum confirmation input. The form renders a `choice`\n * field whose options are the titled `choices`; the caller reads the selection\n * with `readAcceptedChoice`.\n */\nexport function choiceInput(\n key: string,\n message: string,\n choices: readonly { value: string; title: string }[],\n): PendingInput {\n return { key, message, choices };\n}\n\n/**\n * Build a multi-select enum confirmation input. Like `choiceInput` but the\n * form renders `choice` as a string array (`MultiSelectEnumSchema` shape), so\n * the client may accept a subset; the caller reads the accepted set with\n * `readAcceptedMultiChoice`.\n */\nexport function multiSelectInput(\n key: string,\n message: string,\n choices: readonly { value: string; title: string }[],\n): PendingInput {\n return {\n key,\n message,\n choices,\n multi: true,\n };\n}\n\n/**\n * Build an `input_required` result carrying one boolean confirmation per pending\n * item, plus the integrity-protected `requestState` sealing the operation kind\n * and sorted target paths. `mint` is async (HMAC + base64url).\n */\nexport async function buildInputRequired(\n pending: PendingState,\n inputs: readonly PendingInput[],\n): Promise<InputRequiredResult> {\n const inputRequests: Record<string, InputRequest> = {};\n for (const input of inputs) {\n const requestedSchema = input.choices\n ? input.multi\n ? {\n // Multi-select enum (matches MultiSelectEnumSchema): `choice` is a\n // string array; the client may accept a subset of the offered dirs.\n type: 'object' as const,\n properties: {\n choice: {\n type: 'array' as const,\n items: {\n anyOf: input.choices.map((c) => ({ const: c.value, title: c.title })),\n },\n },\n },\n required: ['choice'],\n }\n : {\n type: 'object' as const,\n properties: {\n choice: {\n type: 'string' as const,\n oneOf: input.choices.map((c) => ({ const: c.value, title: c.title })),\n },\n },\n required: ['choice'],\n }\n : {\n type: 'object' as const,\n properties: { confirm: { type: 'boolean' as const, title: 'Confirm' } },\n required: ['confirm'],\n };\n inputRequests[input.key] = inputRequired.elicit({\n message: input.message,\n requestedSchema,\n });\n }\n const requestState = await requestStateCodec.mint({\n op: pending.op,\n paths: [...pending.paths].sort(),\n });\n return inputRequired({ inputRequests, requestState });\n}\n\n/**\n * The shared read-state → `buildInputRequired` → mismatch-throw flow used by\n * every destructive-confirmation handler (delete, move, grant). No verified\n * state, OR a verified state belonging to a different `op` (e.g. a chained\n * call's grant round already resolved and this is now that same call's own\n * delete/move confirmation round), mints a fresh `input_required` for this\n * op. A retry whose verified state matches this op but not the pending path\n * set throws `FsError(INVALID_INPUT)` (R9) — uniformly surfaced as an\n * `isError` tool result by the handler's catch. Returns `undefined` on a\n * matching same-op retry so the caller proceeds.\n *\n * One home for the R9 binding check means a future fix cannot miss two of three\n * sites. The caller supplies `buildInputs` so only the prompt text varies.\n */\nexport interface PendingRoundTripOpts {\n readonly op: PendingOp;\n readonly pending: readonly string[];\n readonly requestState: (() => PendingState | undefined) | undefined;\n /**\n * What the client declared it can do, or `undefined` when this connection\n * cannot say. Only a positively-absent `elicitation` short-circuits; see\n * {@link assertCanElicit}.\n */\n readonly clientCapabilities?: ClientCapabilities | undefined;\n readonly buildInputs: (pending: readonly string[]) => readonly PendingInput[];\n}\n\n/**\n * What to tell the model when the round-trip cannot happen. Each names the way\n * forward that does not need a confirmation, or says plainly that there is\n * none — a dead end the model can report is worth more than a retry loop.\n */\nconst NO_ELICITATION_HINT: Readonly<Record<PendingOp, string>> = {\n delete:\n 'Deleting a non-empty directory needs a confirmation this client cannot show. ' +\n 'Delete the entries inside it individually, or connect a client that declares the elicitation capability.',\n move:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Delete the destination first, or move to a path that does not exist yet.',\n copy:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',\n grant:\n 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +\n 'Call list_roots and use a path under one of the roots it returns.',\n};\n\n/**\n * Fail early, and legibly, when the client cannot answer an embedded request.\n * The SDK checks each `inputRequests` entry against the declared client\n * capabilities and rejects the whole call with `-32021` before anything reaches\n * the wire — a protocol error the model never sees. Throwing `FsError` here\n * instead lands in the tool executor's catch and reaches the model as an\n * `isError` result carrying the workaround.\n *\n * `undefined` capabilities mean the connection cannot report them, NOT that the\n * client has none: proceed and let the SDK decide, exactly as before this check\n * existed.\n */\nfunction assertCanElicit(op: PendingOp, capabilities: ClientCapabilities | undefined): void {\n if (capabilities === undefined) return;\n if (capabilities.elicitation !== undefined) return;\n throw new FsError(ErrorCode.INVALID_INPUT, `${op}: ${NO_ELICITATION_HINT[op]}`);\n}\n\nexport async function pendingRoundTrip(\n opts: PendingRoundTripOpts,\n): Promise<InputRequiredResult | undefined> {\n const state = opts.requestState?.();\n // No verified state yet, OR the verified state belongs to a different\n // flow (e.g. an access-grant round's state is still the retried request's\n // requestState after the grant was applied, and this call is now the\n // destructive-confirmation round for the SAME tool call) — either way,\n // this op has not yet had its own round-trip, so mint a fresh\n // input_required rather than treating a foreign-but-valid state as a\n // tamper/mismatch error.\n if (state?.op !== opts.op) {\n assertCanElicit(opts.op, opts.clientCapabilities);\n return buildInputRequired({ op: opts.op, paths: opts.pending }, opts.buildInputs(opts.pending));\n }\n // Retry for THIS op (R9): the verified state must bind the same pending set.\n if (!pathsEqual(state.paths, opts.pending)) {\n throw new FsError(\n ErrorCode.INVALID_INPUT,\n `${opts.op}: confirmation does not match the requested paths`,\n );\n }\n return undefined;\n}\n\n// Response shapes handed to the SDK's schema-aware `acceptedContent` overload:\n// it returns `undefined` for a missing key, a decline/cancel, a non-elicit\n// response, AND a payload that fails validation — one call covers every refusal\n// case the readers below used to hand-check.\nconst ConfirmContent = z.object({ confirm: z.boolean() });\nconst ChoiceContent = z.object({ choice: z.string() });\nconst MultiChoiceContent = z.object({ choice: z.array(z.string()) });\n\n/**\n * Read one pending item's boolean confirmation from a retried request's\n * `inputResponses`. Returns `true` only when the client explicitly accepted AND\n * the `confirm` field is `true`; every other outcome (decline, cancel, missing\n * key, accept-without-confirm) returns `false`, which the caller reports as\n * `CANCELLED` (R3 proceed, R4/R5 cancelled).\n */\nexport function readAcceptedConfirm(\n responses: Record<string, unknown> | undefined,\n key: string,\n): boolean {\n return acceptedContent(responses, key, ConfirmContent)?.confirm === true;\n}\n\n/**\n * Read one pending item's enum selection from a retried request's\n * `inputResponses`. Returns the chosen `value` only when the client explicitly\n * accepted AND the `choice` field is a string; every other outcome (decline,\n * cancel, missing key, accept-without-choice) returns `undefined`, which the\n * caller reports as `CANCELLED` — same contract as the boolean reader.\n *\n * The shape is SDK-validated, but the returned value is NOT checked for\n * membership in the `choices` offered in the request schema. Callers must\n * compare against the expected values (e.g. `choice === 'overwrite'`) before\n * acting — never echo the string back into a path or trust it as an enum\n * member.\n */\nexport function readAcceptedChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n return acceptedContent(responses, key, ChoiceContent)?.choice;\n}\n\n/**\n * Read one pending item's multi-select enum selection from a retried request's\n * `inputResponses`. Returns the accepted `value`s only when the client\n * explicitly accepted AND `choice` is a string array of strings; every other\n * outcome (decline, cancel, missing key, accept-without-choice, non-array\n * `choice`, non-string elements) returns `undefined`, which the caller reports\n * as `CANCELLED` — same contract as the single-select reader.\n *\n * As with `readAcceptedChoice`, the returned values are NOT checked for\n * membership in the offered `choices`; callers must compare against the\n * expected set before acting.\n */\nexport function readAcceptedMultiChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string[] | undefined {\n return acceptedContent(responses, key, MultiChoiceContent)?.choice;\n}\n\n/**\n * Order-independent equality of two sorted, de-duplicated path lists. Used to\n * compare a recomputed pending set against the `requestState`-bound set (R9):\n * both are sorted by construction, so a straight element-wise compare settles\n * it without allocating.\n */\nfunction pathsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n"]}
1
+ {"version":3,"file":"input-required.js","sourceRoot":"","sources":["../../src/core/input-required.ts"],"names":[],"mappings":"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","sourcesContent":["// Shared infrastructure for the SEP-2577 `input_required` multi-round-trip\n// destructive-confirmation flows (recursive delete, move overwrite, out-of-root\n// access grant). A handler returns `inputRequired(...)` instead of the deprecated\n// push-style server-to-client elicitation request; the client retries the same\n// `tools/call` carrying `inputResponses`, and the handler re-enters from the top\n// reading the verified `requestState` and the accepted responses.\n//\n// `requestState` round-trips through the client (attacker-controlled on\n// re-entry), so it is sealed with the SDK's HMAC-SHA256 codec. The payload binds\n// each confirmation to its operation kind and sorted target-path set (spec R9,\n// R10); the handler additionally rejects any retry whose decoded paths do not\n// match the retried request's parameters (the codec only proves the state was\n// not tampered — it cannot see the current args).\nimport type {\n ClientCapabilities,\n InputRequest,\n InputRequiredResult,\n RequestStateCodec,\n} from '@modelcontextprotocol/server';\nimport {\n acceptedContent,\n createRequestStateCodec,\n inputRequired,\n} from '@modelcontextprotocol/server';\n\nimport { randomBytes } from 'node:crypto';\n\nimport * as z from 'zod/v4';\n\nimport { ErrorCode, FsError } from './errors.js';\nimport { Logger } from './observability.js';\n\n/** The destructive operation a pending confirmation authorizes. */\ntype PendingOp = 'delete' | 'move' | 'copy' | 'grant';\n\n/**\n * Integrity-protected state minted into an `input_required` result and echoed\n * back by the client on retry. `paths` is the sorted canonical set of target\n * paths the confirmation covers; the handler compares it to the retried\n * request's parameters and rejects a mismatch (R9).\n */\nexport interface PendingState {\n readonly op: PendingOp;\n readonly paths: readonly string[];\n}\n\n/** One embedded form-mode confirmation, keyed within the call. */\ninterface PendingInput {\n /** Server-assigned key, unique within the `tools/call`. */\n readonly key: string;\n /** Human-readable prompt for this item. */\n readonly message: string;\n /**\n * When set, the form offers a titled single-select enum (`choice` field)\n * instead of a boolean `confirm`. Each entry is a `{ value, title }` pair.\n */\n readonly choices?: readonly { value: string; title: string }[];\n /**\n * When set with `choices`, the form offers a multi-select enum: `choice` is\n * a string array (`MultiSelectEnumSchema` shape) and the caller reads the\n * accepted set with `readAcceptedMultiChoice`. Single-select otherwise.\n */\n readonly multi?: boolean;\n}\n\n/**\n * HMAC key for the requestState codec. Read once from\n * `FS_REQUEST_STATE_KEY` (UTF-8, must be >=32 bytes); a random\n * 32-byte key is generated at boot when the env var is unset or too short. A\n * per-process key is correct for stdio and the single-node HTTP leg (decision\n * record 11). A server restart invalidates in-flight tokens; the client\n * re-requests, which is fail-closed and safe.\n */\nfunction configuredRequestStateKey(): Uint8Array | undefined {\n const env = process.env['FS_REQUEST_STATE_KEY'];\n if (env) {\n const bytes = Buffer.from(env, 'utf8');\n if (bytes.length >= 32) return bytes;\n Logger.warn(\n `FS_REQUEST_STATE_KEY is ${String(bytes.length)} bytes; 32 are required. Falling back to a random per-boot key — in-flight input_required rounds will not survive a restart.`,\n );\n }\n return undefined;\n}\n\n// Built on first use, not at module load, so an unset env var costs nothing at\n// import and the random per-boot fallback is minted only once something needs it.\nlet codec: RequestStateCodec<PendingState> | undefined;\n\nfunction getRequestStateCodec(): RequestStateCodec<PendingState> {\n codec ??= createRequestStateCodec<PendingState>({\n key: configuredRequestStateKey() ?? randomBytes(32),\n });\n return codec;\n}\n\n/**\n * The codec: `mint` seals a `PendingState` into the opaque wire string a\n * handler returns from `inputRequired({ requestState })`; `verify` drops into\n * `ServerOptions.requestState.verify` and throws on tamper, expiry, or bind\n * mismatch (the seam answers with the frozen `-32602`).\n */\nexport const requestStateCodec: RequestStateCodec<PendingState> = {\n mint: (payload, ctx) => getRequestStateCodec().mint(payload, ctx),\n verify: (state, ctx) => getRequestStateCodec().verify(state, ctx),\n};\n\n/**\n * Build a single-select enum confirmation input. The form renders a `choice`\n * field whose options are the titled `choices`; the caller reads the selection\n * with `readAcceptedChoice`.\n */\nexport function choiceInput(\n key: string,\n message: string,\n choices: readonly { value: string; title: string }[],\n): PendingInput {\n return { key, message, choices };\n}\n\n/**\n * Build a multi-select enum confirmation input. Like `choiceInput` but the\n * form renders `choice` as a string array (`MultiSelectEnumSchema` shape), so\n * the client may accept a subset; the caller reads the accepted set with\n * `readAcceptedMultiChoice`.\n */\nexport function multiSelectInput(\n key: string,\n message: string,\n choices: readonly { value: string; title: string }[],\n): PendingInput {\n return {\n key,\n message,\n choices,\n multi: true,\n };\n}\n\n/**\n * Build an `input_required` result carrying one boolean confirmation per pending\n * item, plus the integrity-protected `requestState` sealing the operation kind\n * and sorted target paths. `mint` is async (HMAC + base64url).\n */\nexport async function buildInputRequired(\n pending: PendingState,\n inputs: readonly PendingInput[],\n): Promise<InputRequiredResult> {\n const inputRequests: Record<string, InputRequest> = {};\n for (const input of inputs) {\n const requestedSchema = input.choices\n ? input.multi\n ? {\n // Multi-select enum (matches MultiSelectEnumSchema): `choice` is a\n // string array; the client may accept a subset of the offered dirs.\n type: 'object' as const,\n properties: {\n choice: {\n type: 'array' as const,\n items: {\n anyOf: input.choices.map((c) => ({ const: c.value, title: c.title })),\n },\n },\n },\n required: ['choice'],\n }\n : {\n type: 'object' as const,\n properties: {\n choice: {\n type: 'string' as const,\n oneOf: input.choices.map((c) => ({ const: c.value, title: c.title })),\n },\n },\n required: ['choice'],\n }\n : {\n type: 'object' as const,\n properties: { confirm: { type: 'boolean' as const, title: 'Confirm' } },\n required: ['confirm'],\n };\n inputRequests[input.key] = inputRequired.elicit({\n message: input.message,\n requestedSchema,\n });\n }\n const requestState = await requestStateCodec.mint({\n op: pending.op,\n paths: [...pending.paths].sort(),\n });\n return inputRequired({ inputRequests, requestState });\n}\n\n/**\n * The shared read-state → `buildInputRequired` → mismatch-throw flow used by\n * every destructive-confirmation handler (delete, move, grant). No verified\n * state, OR a verified state belonging to a different `op` (e.g. a chained\n * call's grant round already resolved and this is now that same call's own\n * delete/move confirmation round), mints a fresh `input_required` for this\n * op. A retry whose verified state matches this op but not the pending path\n * set throws `FsError(INVALID_INPUT)` (R9) — uniformly surfaced as an\n * `isError` tool result by the handler's catch. Returns `undefined` on a\n * matching same-op retry so the caller proceeds.\n *\n * One home for the R9 binding check means a future fix cannot miss two of three\n * sites. The caller supplies `buildInputs` so only the prompt text varies.\n */\ninterface PendingRoundTripOpts {\n readonly op: PendingOp;\n readonly pending: readonly string[];\n readonly requestState: (() => PendingState | undefined) | undefined;\n /**\n * What the client declared it can do, or `undefined` when this connection\n * cannot say. Only a positively-absent `elicitation` short-circuits; see\n * {@link assertCanElicit}.\n */\n readonly clientCapabilities?: ClientCapabilities | undefined;\n readonly buildInputs: (pending: readonly string[]) => readonly PendingInput[];\n}\n\n/**\n * What to tell the model when the round-trip cannot happen. Each names the way\n * forward that does not need a confirmation, or says plainly that there is\n * none — a dead end the model can report is worth more than a retry loop.\n */\nconst NO_ELICITATION_HINT: Readonly<Record<PendingOp, string>> = {\n delete:\n 'Deleting a non-empty directory needs a confirmation this client cannot show. ' +\n 'Delete the entries inside it individually, or connect a client that declares the elicitation capability.',\n move:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Delete the destination first, or move to a path that does not exist yet.',\n copy:\n 'Overwriting an existing destination needs a confirmation this client cannot show. ' +\n 'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',\n grant:\n 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +\n 'Call list_roots and use a path under one of the roots it returns.',\n};\n\n/**\n * Fail early, and legibly, when the client cannot answer an embedded request.\n * The SDK checks each `inputRequests` entry against the declared client\n * capabilities and rejects the whole call with `-32021` before anything reaches\n * the wire — a protocol error the model never sees. Throwing `FsError` here\n * instead lands in the tool executor's catch and reaches the model as an\n * `isError` result carrying the workaround.\n *\n * `undefined` capabilities mean the connection cannot report them, NOT that the\n * client has none: proceed and let the SDK decide, exactly as before this check\n * existed.\n */\nfunction assertCanElicit(op: PendingOp, capabilities: ClientCapabilities | undefined): void {\n if (capabilities === undefined) return;\n if (capabilities.elicitation !== undefined) return;\n throw new FsError(ErrorCode.INVALID_INPUT, `${op}: ${NO_ELICITATION_HINT[op]}`);\n}\n\nexport async function pendingRoundTrip(\n opts: PendingRoundTripOpts,\n): Promise<InputRequiredResult | undefined> {\n const state = opts.requestState?.();\n // No verified state yet, OR the verified state belongs to a different\n // flow (e.g. an access-grant round's state is still the retried request's\n // requestState after the grant was applied, and this call is now the\n // destructive-confirmation round for the SAME tool call) — either way,\n // this op has not yet had its own round-trip, so mint a fresh\n // input_required rather than treating a foreign-but-valid state as a\n // tamper/mismatch error.\n if (state?.op !== opts.op) {\n assertCanElicit(opts.op, opts.clientCapabilities);\n return buildInputRequired({ op: opts.op, paths: opts.pending }, opts.buildInputs(opts.pending));\n }\n // Retry for THIS op (R9): the verified state must bind the same pending set.\n if (!pathsEqual(state.paths, opts.pending)) {\n throw new FsError(\n ErrorCode.INVALID_INPUT,\n `${opts.op}: confirmation does not match the requested paths`,\n );\n }\n return undefined;\n}\n\n// Response shapes handed to the SDK's schema-aware `acceptedContent` overload:\n// it returns `undefined` for a missing key, a decline/cancel, a non-elicit\n// response, AND a payload that fails validation — one call covers every refusal\n// case the readers below used to hand-check.\nconst ConfirmContent = z.object({ confirm: z.boolean() });\nconst ChoiceContent = z.object({ choice: z.string() });\nconst MultiChoiceContent = z.object({ choice: z.array(z.string()) });\n\n/**\n * Round-trip key for `pendingSorted[index]` — the single home of the\n * `confirm_${i}` convention. The build side mints it with the map index; the\n * read side re-derives it from the same sorted pending set the requestState\n * binds (R9), so a format change lands here once instead of at six call\n * sites.\n */\nexport function confirmKey(index: number): string {\n return `confirm_${index}`;\n}\n\n/**\n * Read one pending item's boolean confirmation from a retried request's\n * `inputResponses`. Returns `true` only when the client explicitly accepted AND\n * the `confirm` field is `true`; every other outcome (decline, cancel, missing\n * key, accept-without-confirm) returns `false`, which the caller reports as\n * `CANCELLED` (R3 proceed, R4/R5 cancelled).\n */\nexport function readAcceptedConfirm(\n responses: Record<string, unknown> | undefined,\n key: string,\n): boolean {\n return acceptedContent(responses, key, ConfirmContent)?.confirm === true;\n}\n\n/**\n * Read one pending item's enum selection from a retried request's\n * `inputResponses`. Returns the chosen `value` only when the client explicitly\n * accepted AND the `choice` field is a string; every other outcome (decline,\n * cancel, missing key, accept-without-choice) returns `undefined`, which the\n * caller reports as `CANCELLED` — same contract as the boolean reader.\n *\n * The shape is SDK-validated, but the returned value is NOT checked for\n * membership in the `choices` offered in the request schema. Callers must\n * compare against the expected values (e.g. `choice === 'overwrite'`) before\n * acting — never echo the string back into a path or trust it as an enum\n * member.\n */\nexport function readAcceptedChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string | undefined {\n return acceptedContent(responses, key, ChoiceContent)?.choice;\n}\n\n/**\n * Read one pending item's multi-select enum selection from a retried request's\n * `inputResponses`. Returns the accepted `value`s only when the client\n * explicitly accepted AND `choice` is a string array of strings; every other\n * outcome (decline, cancel, missing key, accept-without-choice, non-array\n * `choice`, non-string elements) returns `undefined`, which the caller reports\n * as `CANCELLED` — same contract as the single-select reader.\n *\n * As with `readAcceptedChoice`, the returned values are NOT checked for\n * membership in the offered `choices`; callers must compare against the\n * expected set before acting.\n */\nexport function readAcceptedMultiChoice(\n responses: Record<string, unknown> | undefined,\n key: string,\n): string[] | undefined {\n return acceptedContent(responses, key, MultiChoiceContent)?.choice;\n}\n\n/**\n * Order-independent equality of two sorted, de-duplicated path lists. Used to\n * compare a recomputed pending set against the `requestState`-bound set (R9):\n * both are sorted by construction, so a straight element-wise compare settles\n * it without allocating.\n */\nfunction pathsEqual(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GACtB,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;AA6DzF,eAAO,MAAM,MAAM;kBACH,YAAY,WAAW,MAAM;oBAG3B,MAAM,WAAW,OAAO,EAAE;oBAG1B,MAAM,WAAW,OAAO,EAAE;qBAGzB,MAAM,WAAW,OAAO,EAAE;qBAG1B,MAAM,WAAW,OAAO,EAAE;CAG5C,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAEjG"}
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,YAAY,GACtB,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,GAAG,OAAO,GAAG,WAAW,CAAC;AA2DzF,eAAO,MAAM,MAAM;kBACH,YAAY,WAAW,MAAM;oBAG3B,MAAM,WAAW,OAAO,EAAE;oBAG1B,MAAM,WAAW,OAAO,EAAE;qBAGzB,MAAM,WAAW,OAAO,EAAE;qBAG1B,MAAM,WAAW,OAAO,EAAE;CAG5C,CAAC;AAEF,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAEjG"}
@@ -1,4 +1,5 @@
1
1
  import { cli } from './config.js';
2
+ import { warnInvalidSetting } from './primitives.js';
2
3
  // RFC 5424 severities, most severe first. A message is emitted when its
3
4
  // severity is at least as high as the configured minimum.
4
5
  const LEVEL_ORDER = [
@@ -22,7 +23,7 @@ function parseLogLevel(raw) {
22
23
  return 'warning';
23
24
  if (isLoggingLevel(raw))
24
25
  return raw;
25
- console.error(`[warning] Invalid FS_LOG_LEVEL value: ${raw} (must be ${LEVEL_ORDER.join('|')}). Using default: info`);
26
+ warnInvalidSetting('FS_LOG_LEVEL', raw, LEVEL_ORDER.join('|'), 'info');
26
27
  return 'info';
27
28
  }
28
29
  // Seeded to the value `parseLogLevel(undefined)` returns, so an unset
@@ -1 +1 @@
1
- {"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"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","sourcesContent":["import { cli } from './config.js';\n\nexport type LoggingLevel =\n 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';\n\n// RFC 5424 severities, most severe first. A message is emitted when its\n// severity is at least as high as the configured minimum.\nconst LEVEL_ORDER: readonly LoggingLevel[] = [\n 'emergency',\n 'alert',\n 'critical',\n 'error',\n 'warning',\n 'notice',\n 'info',\n 'debug',\n];\n\nfunction isLoggingLevel(value: string): value is LoggingLevel {\n return (LEVEL_ORDER as readonly string[]).includes(value);\n}\n\nfunction parseLogLevel(raw: string | undefined): LoggingLevel {\n if (!raw) return 'info';\n // `warn` is the common short form; the canonical RFC 5424 level is `warning`.\n if (raw === 'warn') return 'warning';\n if (isLoggingLevel(raw)) return raw;\n console.error(\n `[warning] Invalid FS_LOG_LEVEL value: ${raw} (must be ${LEVEL_ORDER.join('|')}). Using default: info`,\n );\n return 'info';\n}\n\n// Seeded to the value `parseLogLevel(undefined)` returns, so an unset\n// LOG_LEVEL — the initial `cachedRaw` — needs no first-call special case.\nlet cachedRaw: string | undefined;\nlet cachedLevel: LoggingLevel = 'info';\n\n/**\n * Minimum severity that reaches stderr, from `FS_LOG_LEVEL` / `--log-level`.\n * Memoized on the raw value, not resolved once: `cli.logLevel` lands after\n * `parseArgs`, and writes happen before that. Keying on the raw string keeps it\n * live while the invalid-value warning fires once per setting, not per line.\n */\nfunction getLogLevel(): LoggingLevel {\n const raw = (cli.logLevel ?? process.env['FS_LOG_LEVEL'])?.trim().toLowerCase();\n if (raw !== cachedRaw) {\n cachedRaw = raw;\n cachedLevel = parseLogLevel(raw);\n }\n return cachedLevel;\n}\n\n/** True when `level` is at least as severe as the configured minimum. */\nfunction isLevelEnabled(level: LoggingLevel, minimum: LoggingLevel = getLogLevel()): boolean {\n return LEVEL_ORDER.indexOf(level) <= LEVEL_ORDER.indexOf(minimum);\n}\n\nfunction write(level: LoggingLevel, message: string, args: readonly unknown[]): void {\n if (!isLevelEnabled(level)) return;\n const prefix = `[${level}]`;\n console.error(`${prefix} ${message}`, ...args);\n}\n\nexport const Logger = {\n emit: (level: LoggingLevel, message: string) => {\n write(level, message, []);\n },\n info: (message: string, ...args: unknown[]) => {\n write('info', message, args);\n },\n warn: (message: string, ...args: unknown[]) => {\n write('warning', message, args);\n },\n error: (message: string, ...args: unknown[]) => {\n write('error', message, args);\n },\n debug: (message: string, ...args: unknown[]) => {\n write('debug', message, args);\n },\n};\n\nexport function logRuntimeFailure(id: string, scope: string, method: string, error: unknown): void {\n write('error', `Runtime failure: ${id} [${scope}.${method}]`, [error]);\n}\n"]}
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../../src/core/observability.ts"],"names":[],"mappings":"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","sourcesContent":["import { cli } from './config.js';\nimport { warnInvalidSetting } from './primitives.js';\n\nexport type LoggingLevel =\n 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';\n\n// RFC 5424 severities, most severe first. A message is emitted when its\n// severity is at least as high as the configured minimum.\nconst LEVEL_ORDER: readonly LoggingLevel[] = [\n 'emergency',\n 'alert',\n 'critical',\n 'error',\n 'warning',\n 'notice',\n 'info',\n 'debug',\n];\n\nfunction isLoggingLevel(value: string): value is LoggingLevel {\n return (LEVEL_ORDER as readonly string[]).includes(value);\n}\n\nfunction parseLogLevel(raw: string | undefined): LoggingLevel {\n if (!raw) return 'info';\n // `warn` is the common short form; the canonical RFC 5424 level is `warning`.\n if (raw === 'warn') return 'warning';\n if (isLoggingLevel(raw)) return raw;\n warnInvalidSetting('FS_LOG_LEVEL', raw, LEVEL_ORDER.join('|'), 'info');\n return 'info';\n}\n\n// Seeded to the value `parseLogLevel(undefined)` returns, so an unset\n// LOG_LEVEL — the initial `cachedRaw` — needs no first-call special case.\nlet cachedRaw: string | undefined;\nlet cachedLevel: LoggingLevel = 'info';\n\n/**\n * Minimum severity that reaches stderr, from `FS_LOG_LEVEL` / `--log-level`.\n * Memoized on the raw value, not resolved once: `cli.logLevel` lands after\n * `parseArgs`, and writes happen before that. Keying on the raw string keeps it\n * live while the invalid-value warning fires once per setting, not per line.\n */\nfunction getLogLevel(): LoggingLevel {\n const raw = (cli.logLevel ?? process.env['FS_LOG_LEVEL'])?.trim().toLowerCase();\n if (raw !== cachedRaw) {\n cachedRaw = raw;\n cachedLevel = parseLogLevel(raw);\n }\n return cachedLevel;\n}\n\n/** True when `level` is at least as severe as the configured minimum. */\nfunction isLevelEnabled(level: LoggingLevel, minimum: LoggingLevel = getLogLevel()): boolean {\n return LEVEL_ORDER.indexOf(level) <= LEVEL_ORDER.indexOf(minimum);\n}\n\nfunction write(level: LoggingLevel, message: string, args: readonly unknown[]): void {\n if (!isLevelEnabled(level)) return;\n const prefix = `[${level}]`;\n console.error(`${prefix} ${message}`, ...args);\n}\n\nexport const Logger = {\n emit: (level: LoggingLevel, message: string) => {\n write(level, message, []);\n },\n info: (message: string, ...args: unknown[]) => {\n write('info', message, args);\n },\n warn: (message: string, ...args: unknown[]) => {\n write('warning', message, args);\n },\n error: (message: string, ...args: unknown[]) => {\n write('error', message, args);\n },\n debug: (message: string, ...args: unknown[]) => {\n write('debug', message, args);\n },\n};\n\nexport function logRuntimeFailure(id: string, scope: string, method: string, error: unknown): void {\n write('error', `Runtime failure: ${id} [${scope}.${method}]`, [error]);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"path-completer.d.ts","sourceRoot":"","sources":["../../src/core/path-completer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAuM3C,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAE1B,SAAS,EAAE,SAAS;IAI1B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAa/C,OAAO,CAAC,aAAa;YAOP,YAAY;CA8B3B"}
1
+ {"version":3,"file":"path-completer.d.ts","sourceRoot":"","sources":["../../src/core/path-completer.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAwL3C,qBAAa,aAAa;IACxB,OAAO,CAAC,KAAK,CAAiC;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;gBAE1B,SAAS,EAAE,SAAS;IAI1B,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAa/C,OAAO,CAAC,aAAa;YAOP,YAAY;CA8B3B"}
@@ -74,12 +74,7 @@ function withDirectorySeparator(value) {
74
74
  return value.endsWith(sep) ? value : `${value}${sep}`;
75
75
  }
76
76
  function collectAllowedRoots(allowed, predicate) {
77
- const matches = [];
78
- for (const root of allowed) {
79
- if (predicate(root))
80
- matches.push(withDirectorySeparator(root));
81
- }
82
- return matches;
77
+ return allowed.filter(predicate).map(withDirectorySeparator);
83
78
  }
84
79
  function findRootPrefixMatches(currentValue, allowed) {
85
80
  const rootPrefix = parseNamedRootInput(currentValue)?.rootName.toLowerCase() ?? '';
@@ -97,27 +92,9 @@ function findMatchingRoots(searchDir, prefix, allowed) {
97
92
  return basename(root).toLowerCase().startsWith(lowerPrefix);
98
93
  });
99
94
  }
100
- function sortCompletionMatches(matches) {
101
- const sepCode = sep.charCodeAt(0);
102
- matches.sort((left, right) => {
103
- const leftIsDir = left.charCodeAt(left.length - 1) === sepCode;
104
- const rightIsDir = right.charCodeAt(right.length - 1) === sepCode;
105
- if (leftIsDir && !rightIsDir)
106
- return -1;
107
- if (!leftIsDir && rightIsDir)
108
- return 1;
109
- return left.localeCompare(right);
110
- });
111
- }
112
- function mergeCompletionMatches(...matchGroups) {
113
- const uniqueMatches = new Set();
114
- for (const group of matchGroups) {
115
- for (const match of group)
116
- uniqueMatches.add(match);
117
- }
118
- const merged = Array.from(uniqueMatches);
119
- sortCompletionMatches(merged);
120
- return merged;
95
+ function mergeCompletionMatches(dirMatches, rootMatches) {
96
+ const isDir = (v) => v.endsWith(sep);
97
+ return [...new Set([...dirMatches, ...rootMatches])].sort((left, right) => Number(isDir(right)) - Number(isDir(left)) || left.localeCompare(right));
121
98
  }
122
99
  async function findMatchesInDirectory(searchDir, prefix, allowed, isSensitive) {
123
100
  const matches = [];
@@ -1 +1 @@
1
- {"version":3,"file":"path-completer.js","sourceRoot":"","sources":["../../src/core/path-completer.ts"],"names":[],"mappings":"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","sourcesContent":["import { opendir, stat } from 'node:fs/promises';\nimport { basename, dirname, isAbsolute, join, parse, resolve, sep } from 'node:path';\n\nimport { isNodeError, isNotFoundErrno, rethrowIfAborted } from './errors.js';\nimport { Logger } from './observability.js';\nimport { isPathWithinDirectories, isSamePath, normalizePath } from './path-utils.js';\nimport type { PathGuard } from './path.js';\nimport { resolveRealPath } from './path.js';\nimport { isSlash, toPosixPath } from './primitives.js';\n\nconst MAX_COMPLETION_ITEMS = 100;\nconst COMPLETION_RATE_LIMIT_MS = 100;\nconst MAX_COMPLETION_CACHE_KEYS = 128;\n\ninterface CacheEntry {\n ms: number;\n result: string[];\n}\n\n// ─── pure path-completion helpers (no instance state) ───────────────────────\n// Formerly static methods on PathCompleter. They take `allowed` explicitly and\n// touch no state, so they read as plain functions rather than a class used as\n// a namespace. PathCompleter keeps only the cache and the path guard.\n\nfunction hasTrailingSeparator(value: string): boolean {\n return value.length > 0 && isSlash(value.charCodeAt(value.length - 1));\n}\n\nfunction resolveFromBase(\n base: string,\n rawValue: string,\n trailingSeparator: boolean,\n): { searchDir: string; prefix: string } {\n const normalizedValue = normalizePath(resolve(base, rawValue));\n if (trailingSeparator) return { searchDir: normalizedValue, prefix: '' };\n return {\n searchDir: dirname(normalizedValue),\n prefix: basename(normalizedValue),\n };\n}\n\nfunction parseNamedRootInput(value: string): { rootName: string; remainder: string } | undefined {\n const normalizedInput = toPosixPath(value);\n if (!normalizedInput) return undefined;\n const slashIndex = normalizedInput.indexOf('/');\n if (slashIndex === -1) return { rootName: normalizedInput, remainder: '' };\n const rootName = normalizedInput.slice(0, slashIndex);\n if (!rootName) return undefined;\n return { rootName, remainder: normalizedInput.slice(slashIndex + 1) };\n}\n\nfunction findAllowedRootByName(rootName: string, allowed: readonly string[]): string | undefined {\n const normalizedRootName = rootName.toLowerCase();\n return allowed.find((candidate) => basename(candidate).toLowerCase() === normalizedRootName);\n}\n\nfunction resolveNamedRootContext(\n currentValue: string,\n allowed: readonly string[],\n): { searchDir: string; prefix: string } | undefined {\n const parsed = parseNamedRootInput(currentValue);\n if (!parsed) return undefined;\n const root = findAllowedRootByName(parsed.rootName, allowed);\n if (!root) return undefined;\n const trailingSeparator = hasTrailingSeparator(currentValue);\n return resolveFromBase(root, parsed.remainder, trailingSeparator);\n}\n\nasync function isAllowedCompletionDirectory(\n path: string,\n allowed: readonly string[],\n): Promise<boolean> {\n if (!isPathWithinDirectories(path, allowed)) return false;\n try {\n const stats = await stat(path);\n if (!stats.isDirectory()) return false;\n const real = await resolveRealPath(path);\n return real !== null && isPathWithinDirectories(real, allowed);\n } catch (err) {\n if (!isNotFoundErrno(err) && (!isNodeError(err) || err.code !== 'EACCES')) {\n Logger.debug('isAllowedCompletionDirectory: unexpected probe error', {\n path,\n error: String(err),\n });\n }\n return false;\n }\n}\n\nfunction withDirectorySeparator(value: string): string {\n return value.endsWith(sep) ? value : `${value}${sep}`;\n}\n\nfunction collectAllowedRoots(\n allowed: readonly string[],\n predicate: (root: string) => boolean,\n): string[] {\n const matches: string[] = [];\n for (const root of allowed) {\n if (predicate(root)) matches.push(withDirectorySeparator(root));\n }\n return matches;\n}\n\nfunction findRootPrefixMatches(currentValue: string, allowed: readonly string[]): string[] {\n const rootPrefix = parseNamedRootInput(currentValue)?.rootName.toLowerCase() ?? '';\n if (!rootPrefix) return collectAllowedRoots(allowed, () => true);\n return collectAllowedRoots(allowed, (root) =>\n basename(root).toLowerCase().startsWith(rootPrefix),\n );\n}\n\nfunction findMatchingRoots(\n searchDir: string,\n prefix: string,\n allowed: readonly string[],\n): string[] {\n const lowerPrefix = prefix.toLowerCase();\n const normalizedSearchDir = normalizePath(searchDir);\n return collectAllowedRoots(allowed, (root) => {\n const rootDir = dirname(root);\n if (!isSamePath(rootDir, normalizedSearchDir)) return false;\n return basename(root).toLowerCase().startsWith(lowerPrefix);\n });\n}\n\nfunction sortCompletionMatches(matches: string[]): void {\n const sepCode = sep.charCodeAt(0);\n matches.sort((left, right) => {\n const leftIsDir = left.charCodeAt(left.length - 1) === sepCode;\n const rightIsDir = right.charCodeAt(right.length - 1) === sepCode;\n if (leftIsDir && !rightIsDir) return -1;\n if (!leftIsDir && rightIsDir) return 1;\n return left.localeCompare(right);\n });\n}\n\nfunction mergeCompletionMatches(...matchGroups: readonly (readonly string[])[]): string[] {\n const uniqueMatches = new Set<string>();\n for (const group of matchGroups) {\n for (const match of group) uniqueMatches.add(match);\n }\n const merged = Array.from(uniqueMatches);\n sortCompletionMatches(merged);\n return merged;\n}\n\nasync function findMatchesInDirectory(\n searchDir: string,\n prefix: string,\n allowed: readonly string[],\n isSensitive?: (path: string) => boolean,\n): Promise<string[]> {\n const matches: string[] = [];\n if (!(await isAllowedCompletionDirectory(searchDir, allowed))) return matches;\n try {\n // Stream via opendir and collect every match; the MAX_COMPLETION_ITEMS\n // cap is applied AFTER the alphabetical sort in mergeCompletionMatches\n // (slice at the call site). Capping here would keep the opendir-first\n // 100, not the alphabetically-first 100 — the sort would only reorder\n // an already-arbitrary subset.\n const dir = await opendir(searchDir);\n try {\n const lowerPrefix = prefix.toLowerCase();\n for await (const entry of dir) {\n if (prefix !== '' && !entry.name.toLowerCase().startsWith(lowerPrefix)) continue;\n const fullPath = join(searchDir, entry.name);\n if (isSensitive?.(fullPath)) continue;\n matches.push(entry.isDirectory() ? `${fullPath}${sep}` : fullPath);\n }\n } finally {\n await dir.close().catch(() => {\n /* dir already closed or opendir never resolved */\n });\n }\n } catch (err) {\n if (!isNodeError(err) || (err.code !== 'ENOENT' && err.code !== 'EACCES')) {\n Logger.warn('PathCompleter.findMatchesInDirectory: readdir failed', {\n searchDir,\n error: String(err),\n });\n }\n }\n return matches;\n}\n\nfunction getSearchContext(\n currentValue: string,\n allowed: readonly string[],\n): { searchDir: string; prefix: string } | undefined {\n const trailingSeparator = hasTrailingSeparator(currentValue);\n if (isAbsolute(currentValue)) {\n return resolveFromBase(parse(currentValue).root || sep, currentValue, trailingSeparator);\n }\n const namedRootContext = resolveNamedRootContext(currentValue, allowed);\n if (namedRootContext) return namedRootContext;\n if (allowed.length === 1) {\n const base = allowed[0];\n if (base) return resolveFromBase(base, currentValue, trailingSeparator);\n }\n return undefined;\n}\n\n// ─── PathCompleter: cache + path guard ───────────────────────────────────────\n\nexport class PathCompleter {\n private cache = new Map<string, CacheEntry>();\n private readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n async suggest(value: string): Promise<string[]> {\n const now = Date.now();\n const cacheEntry = this.cache.get(value);\n\n if (cacheEntry && now - cacheEntry.ms < COMPLETION_RATE_LIMIT_MS) {\n return cacheEntry.result;\n }\n\n const results = await this.completePath(value);\n this.setCacheValue(value, { ms: now, result: results });\n return results;\n }\n\n private setCacheValue(key: string, entry: CacheEntry): void {\n // Entries are stale after COMPLETION_RATE_LIMIT_MS anyway, so the cap only\n // has to bound memory — dropping the whole map beats per-key LRU eviction.\n if (this.cache.size >= MAX_COMPLETION_CACHE_KEYS) this.cache.clear();\n this.cache.set(key, entry);\n }\n\n private async completePath(value: string): Promise<string[]> {\n const allowed = this.pathGuard.getAllowedDirectories();\n\n try {\n if (!value) {\n return allowed.slice(0, MAX_COMPLETION_ITEMS);\n }\n\n const context = getSearchContext(value, allowed);\n if (!context) {\n return findRootPrefixMatches(value, allowed).slice(0, MAX_COMPLETION_ITEMS);\n }\n\n const { searchDir, prefix } = context;\n const dirMatches = await findMatchesInDirectory(\n searchDir,\n prefix,\n allowed,\n this.pathGuard.isSensitive.bind(this.pathGuard),\n );\n const rootMatches = findMatchingRoots(searchDir, prefix, allowed);\n return mergeCompletionMatches(dirMatches, rootMatches).slice(0, MAX_COMPLETION_ITEMS);\n } catch (error) {\n rethrowIfAborted(error);\n Logger.warn('PathCompleter: completion failed, returning empty list', {\n error: error instanceof Error ? (error.stack ?? error.message) : String(error),\n });\n return [];\n }\n }\n}\n"]}
1
+ {"version":3,"file":"path-completer.js","sourceRoot":"","sources":["../../src/core/path-completer.ts"],"names":[],"mappings":"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","sourcesContent":["import { opendir, stat } from 'node:fs/promises';\nimport { basename, dirname, isAbsolute, join, parse, resolve, sep } from 'node:path';\n\nimport { isNodeError, isNotFoundErrno, rethrowIfAborted } from './errors.js';\nimport { Logger } from './observability.js';\nimport { isPathWithinDirectories, isSamePath, normalizePath } from './path-utils.js';\nimport type { PathGuard } from './path.js';\nimport { resolveRealPath } from './path.js';\nimport { isSlash, toPosixPath } from './primitives.js';\n\nconst MAX_COMPLETION_ITEMS = 100;\nconst COMPLETION_RATE_LIMIT_MS = 100;\nconst MAX_COMPLETION_CACHE_KEYS = 128;\n\ninterface CacheEntry {\n ms: number;\n result: string[];\n}\n\n// ─── pure path-completion helpers (no instance state) ───────────────────────\n// Formerly static methods on PathCompleter. They take `allowed` explicitly and\n// touch no state, so they read as plain functions rather than a class used as\n// a namespace. PathCompleter keeps only the cache and the path guard.\n\nfunction hasTrailingSeparator(value: string): boolean {\n return value.length > 0 && isSlash(value.charCodeAt(value.length - 1));\n}\n\nfunction resolveFromBase(\n base: string,\n rawValue: string,\n trailingSeparator: boolean,\n): { searchDir: string; prefix: string } {\n const normalizedValue = normalizePath(resolve(base, rawValue));\n if (trailingSeparator) return { searchDir: normalizedValue, prefix: '' };\n return {\n searchDir: dirname(normalizedValue),\n prefix: basename(normalizedValue),\n };\n}\n\nfunction parseNamedRootInput(value: string): { rootName: string; remainder: string } | undefined {\n const normalizedInput = toPosixPath(value);\n if (!normalizedInput) return undefined;\n const slashIndex = normalizedInput.indexOf('/');\n if (slashIndex === -1) return { rootName: normalizedInput, remainder: '' };\n const rootName = normalizedInput.slice(0, slashIndex);\n if (!rootName) return undefined;\n return { rootName, remainder: normalizedInput.slice(slashIndex + 1) };\n}\n\nfunction findAllowedRootByName(rootName: string, allowed: readonly string[]): string | undefined {\n const normalizedRootName = rootName.toLowerCase();\n return allowed.find((candidate) => basename(candidate).toLowerCase() === normalizedRootName);\n}\n\nfunction resolveNamedRootContext(\n currentValue: string,\n allowed: readonly string[],\n): { searchDir: string; prefix: string } | undefined {\n const parsed = parseNamedRootInput(currentValue);\n if (!parsed) return undefined;\n const root = findAllowedRootByName(parsed.rootName, allowed);\n if (!root) return undefined;\n const trailingSeparator = hasTrailingSeparator(currentValue);\n return resolveFromBase(root, parsed.remainder, trailingSeparator);\n}\n\nasync function isAllowedCompletionDirectory(\n path: string,\n allowed: readonly string[],\n): Promise<boolean> {\n if (!isPathWithinDirectories(path, allowed)) return false;\n try {\n const stats = await stat(path);\n if (!stats.isDirectory()) return false;\n const real = await resolveRealPath(path);\n return real !== null && isPathWithinDirectories(real, allowed);\n } catch (err) {\n if (!isNotFoundErrno(err) && (!isNodeError(err) || err.code !== 'EACCES')) {\n Logger.debug('isAllowedCompletionDirectory: unexpected probe error', {\n path,\n error: String(err),\n });\n }\n return false;\n }\n}\n\nfunction withDirectorySeparator(value: string): string {\n return value.endsWith(sep) ? value : `${value}${sep}`;\n}\n\nfunction collectAllowedRoots(\n allowed: readonly string[],\n predicate: (root: string) => boolean,\n): string[] {\n return allowed.filter(predicate).map(withDirectorySeparator);\n}\n\nfunction findRootPrefixMatches(currentValue: string, allowed: readonly string[]): string[] {\n const rootPrefix = parseNamedRootInput(currentValue)?.rootName.toLowerCase() ?? '';\n if (!rootPrefix) return collectAllowedRoots(allowed, () => true);\n return collectAllowedRoots(allowed, (root) =>\n basename(root).toLowerCase().startsWith(rootPrefix),\n );\n}\n\nfunction findMatchingRoots(\n searchDir: string,\n prefix: string,\n allowed: readonly string[],\n): string[] {\n const lowerPrefix = prefix.toLowerCase();\n const normalizedSearchDir = normalizePath(searchDir);\n return collectAllowedRoots(allowed, (root) => {\n const rootDir = dirname(root);\n if (!isSamePath(rootDir, normalizedSearchDir)) return false;\n return basename(root).toLowerCase().startsWith(lowerPrefix);\n });\n}\n\nfunction mergeCompletionMatches(\n dirMatches: readonly string[],\n rootMatches: readonly string[],\n): string[] {\n const isDir = (v: string): boolean => v.endsWith(sep);\n return [...new Set([...dirMatches, ...rootMatches])].sort(\n (left, right) => Number(isDir(right)) - Number(isDir(left)) || left.localeCompare(right),\n );\n}\n\nasync function findMatchesInDirectory(\n searchDir: string,\n prefix: string,\n allowed: readonly string[],\n isSensitive?: (path: string) => boolean,\n): Promise<string[]> {\n const matches: string[] = [];\n if (!(await isAllowedCompletionDirectory(searchDir, allowed))) return matches;\n try {\n // Stream via opendir and collect every match; the MAX_COMPLETION_ITEMS\n // cap is applied AFTER the alphabetical sort in mergeCompletionMatches\n // (slice at the call site). Capping here would keep the opendir-first\n // 100, not the alphabetically-first 100 — the sort would only reorder\n // an already-arbitrary subset.\n const dir = await opendir(searchDir);\n try {\n const lowerPrefix = prefix.toLowerCase();\n for await (const entry of dir) {\n if (prefix !== '' && !entry.name.toLowerCase().startsWith(lowerPrefix)) continue;\n const fullPath = join(searchDir, entry.name);\n if (isSensitive?.(fullPath)) continue;\n matches.push(entry.isDirectory() ? `${fullPath}${sep}` : fullPath);\n }\n } finally {\n await dir.close().catch(() => {\n /* dir already closed or opendir never resolved */\n });\n }\n } catch (err) {\n if (!isNodeError(err) || (err.code !== 'ENOENT' && err.code !== 'EACCES')) {\n Logger.warn('PathCompleter.findMatchesInDirectory: readdir failed', {\n searchDir,\n error: String(err),\n });\n }\n }\n return matches;\n}\n\nfunction getSearchContext(\n currentValue: string,\n allowed: readonly string[],\n): { searchDir: string; prefix: string } | undefined {\n const trailingSeparator = hasTrailingSeparator(currentValue);\n if (isAbsolute(currentValue)) {\n return resolveFromBase(parse(currentValue).root || sep, currentValue, trailingSeparator);\n }\n const namedRootContext = resolveNamedRootContext(currentValue, allowed);\n if (namedRootContext) return namedRootContext;\n if (allowed.length === 1) {\n const base = allowed[0];\n if (base) return resolveFromBase(base, currentValue, trailingSeparator);\n }\n return undefined;\n}\n\n// ─── PathCompleter: cache + path guard ───────────────────────────────────────\n\nexport class PathCompleter {\n private cache = new Map<string, CacheEntry>();\n private readonly pathGuard: PathGuard;\n\n constructor(pathGuard: PathGuard) {\n this.pathGuard = pathGuard;\n }\n\n async suggest(value: string): Promise<string[]> {\n const now = Date.now();\n const cacheEntry = this.cache.get(value);\n\n if (cacheEntry && now - cacheEntry.ms < COMPLETION_RATE_LIMIT_MS) {\n return cacheEntry.result;\n }\n\n const results = await this.completePath(value);\n this.setCacheValue(value, { ms: now, result: results });\n return results;\n }\n\n private setCacheValue(key: string, entry: CacheEntry): void {\n // Entries are stale after COMPLETION_RATE_LIMIT_MS anyway, so the cap only\n // has to bound memory — dropping the whole map beats per-key LRU eviction.\n if (this.cache.size >= MAX_COMPLETION_CACHE_KEYS) this.cache.clear();\n this.cache.set(key, entry);\n }\n\n private async completePath(value: string): Promise<string[]> {\n const allowed = this.pathGuard.getAllowedDirectories();\n\n try {\n if (!value) {\n return allowed.slice(0, MAX_COMPLETION_ITEMS);\n }\n\n const context = getSearchContext(value, allowed);\n if (!context) {\n return findRootPrefixMatches(value, allowed).slice(0, MAX_COMPLETION_ITEMS);\n }\n\n const { searchDir, prefix } = context;\n const dirMatches = await findMatchesInDirectory(\n searchDir,\n prefix,\n allowed,\n this.pathGuard.isSensitive.bind(this.pathGuard),\n );\n const rootMatches = findMatchingRoots(searchDir, prefix, allowed);\n return mergeCompletionMatches(dirMatches, rootMatches).slice(0, MAX_COMPLETION_ITEMS);\n } catch (error) {\n rethrowIfAborted(error);\n Logger.warn('PathCompleter: completion failed, returning empty list', {\n error: error instanceof Error ? (error.stack ?? error.message) : String(error),\n });\n return [];\n }\n }\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  export declare function getReservedDeviceNameForPath(requestedPath: string): string | undefined;
2
2
  export declare function isWindowsDriveRelativePath(requestedPath: string): boolean;
3
3
  export declare function normalizePath(p: string): string;
4
- export declare const IS_CASE_INSENSITIVE_FS: boolean;
4
+ export declare function normalizeCaseForComparison(value: string): string;
5
5
  export declare function isSamePath(left: string, right: string): boolean;
6
6
  export declare function normalizeAllowedDirectory(dir: string): string;
7
7
  export declare function isPathInsideDirectory(normalizedDirectory: string, normalizedCandidate: string): boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAwCA,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAStF;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAkBzE;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/C;AAED,eAAO,MAAM,sBAAsB,SAAwC,CAAC;AAM5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7D;AAED,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAST;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAET"}
1
+ {"version":3,"file":"path-utils.d.ts","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"AAwCA,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAStF;AAED,wBAAgB,0BAA0B,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAkBzE;AAUD,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAa/C;AAID,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAO/D;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAa7D;AAED,wBAAgB,qBAAqB,CACnC,mBAAmB,EAAE,MAAM,EAC3B,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAST;AAED,wBAAgB,uBAAuB,CACrC,cAAc,EAAE,MAAM,EACtB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,OAAO,CAET"}
@@ -80,8 +80,8 @@ export function normalizePath(p) {
80
80
  }
81
81
  return resolved;
82
82
  }
83
- export const IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';
84
- function normalizeCaseForComparison(value) {
83
+ const IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';
84
+ export function normalizeCaseForComparison(value) {
85
85
  return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;
86
86
  }
87
87
  export function isSamePath(left, right) {
@@ -1 +1 @@
1
- {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir, platform } from 'node:os';\nimport { join, parse, resolve, sep } from 'node:path';\n\nimport { IS_WINDOWS, isAlpha, isSlash } from './primitives.js';\n\nconst CHAR_COLON = 58;\nconst HOMEDIR = homedir();\n\nconst RESERVED_DEVICE_NAMES = new Set<string>([\n 'CON',\n 'PRN',\n 'AUX',\n 'NUL',\n 'COM1',\n 'COM2',\n 'COM3',\n 'COM4',\n 'COM5',\n 'COM6',\n 'COM7',\n 'COM8',\n 'COM9',\n 'LPT1',\n 'LPT2',\n 'LPT3',\n 'LPT4',\n 'LPT5',\n 'LPT6',\n 'LPT7',\n 'LPT8',\n 'LPT9',\n]);\n\nfunction getReservedDeviceName(segment: string): string | undefined {\n const trimmed = segment.replace(/[. ]+$/, '');\n const withoutStream = trimmed.split(':')[0] ?? '';\n const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();\n return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;\n}\n\nexport function getReservedDeviceNameForPath(requestedPath: string): string | undefined {\n const segments = requestedPath.split(/[\\\\/]/u);\n for (const segment of segments) {\n const reserved = getReservedDeviceName(segment);\n if (reserved) {\n return reserved;\n }\n }\n return undefined;\n}\n\nexport function isWindowsDriveRelativePath(requestedPath: string): boolean {\n // Check on all platforms so cross-platform clients cannot smuggle drive-relative\n // inputs (e.g. C:relative) to a POSIX-hosted server where path.resolve would\n // silently expand them relative to CWD.\n if (requestedPath.length < 2) {\n return false;\n }\n if (requestedPath.charCodeAt(1) !== CHAR_COLON) {\n return false;\n }\n if (!isAlpha(requestedPath.charCodeAt(0))) {\n return false;\n }\n\n if (requestedPath.length === 2) {\n return true;\n }\n return !isSlash(requestedPath.charCodeAt(2));\n}\n\nfunction expandHome(filepath: string): string {\n if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\\\')) {\n const rest = filepath.slice(1).replace(/^[/\\\\]+/, '');\n return rest ? join(HOMEDIR, rest) : HOMEDIR;\n }\n return filepath;\n}\n\nexport function normalizePath(p: string): string {\n const resolved = resolve(expandHome(p));\n\n // On Windows only the drive letter is lowercased (e.g. \"C:\\Foo\\Bar\").\n // The rest of the path retains its original casing.\n // IMPORTANT: callers must use isSamePath / isPathInsideDirectory for all\n // equality and containment checks — never raw string equality — because\n // those helpers apply full case-folding before comparing.\n if (IS_WINDOWS && resolved.length >= 2 && resolved.charCodeAt(1) === CHAR_COLON) {\n return resolved.charAt(0).toLowerCase() + resolved.slice(1);\n }\n\n return resolved;\n}\n\nexport const IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';\n\nfunction normalizeCaseForComparison(value: string): string {\n return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;\n}\n\nexport function isSamePath(left: string, right: string): boolean {\n if (left === right) {\n return true;\n }\n const leftResolved = normalizeCaseForComparison(resolve(left));\n const rightResolved = normalizeCaseForComparison(resolve(right));\n return leftResolved === rightResolved;\n}\n\nexport function normalizeAllowedDirectory(dir: string): string {\n const trimmed = dir.trim();\n if (trimmed.length === 0) return '';\n\n const normalized = normalizePath(trimmed);\n const { root } = parse(normalized);\n\n // Keep filesystem roots as-is (\"/\", \"c:\\\\\", \"\\\\\\\\server\\\\share\\\\\").\n if (isSamePath(normalized, root)) {\n return root;\n }\n\n return normalized.length > 1 && normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;\n}\n\nexport function isPathInsideDirectory(\n normalizedDirectory: string,\n normalizedCandidate: string,\n): boolean {\n const root = normalizeCaseForComparison(normalizedDirectory);\n const candidate = normalizeCaseForComparison(normalizedCandidate);\n\n if (root === candidate) return true;\n if (!candidate.startsWith(root)) return false;\n\n if (isSlash(root.charCodeAt(root.length - 1))) return true;\n return isSlash(candidate.charCodeAt(root.length));\n}\n\nexport function isPathWithinDirectories(\n normalizedPath: string,\n allowedDirs: readonly string[],\n): boolean {\n return allowedDirs.some((dir) => isPathInsideDirectory(dir, normalizedPath));\n}\n"]}
1
+ {"version":3,"file":"path-utils.js","sourceRoot":"","sources":["../../src/core/path-utils.ts"],"names":[],"mappings":"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","sourcesContent":["import { homedir, platform } from 'node:os';\nimport { join, parse, resolve, sep } from 'node:path';\n\nimport { IS_WINDOWS, isAlpha, isSlash } from './primitives.js';\n\nconst CHAR_COLON = 58;\nconst HOMEDIR = homedir();\n\nconst RESERVED_DEVICE_NAMES = new Set<string>([\n 'CON',\n 'PRN',\n 'AUX',\n 'NUL',\n 'COM1',\n 'COM2',\n 'COM3',\n 'COM4',\n 'COM5',\n 'COM6',\n 'COM7',\n 'COM8',\n 'COM9',\n 'LPT1',\n 'LPT2',\n 'LPT3',\n 'LPT4',\n 'LPT5',\n 'LPT6',\n 'LPT7',\n 'LPT8',\n 'LPT9',\n]);\n\nfunction getReservedDeviceName(segment: string): string | undefined {\n const trimmed = segment.replace(/[. ]+$/, '');\n const withoutStream = trimmed.split(':')[0] ?? '';\n const baseName = (withoutStream.split('.')[0] ?? '').toUpperCase();\n return RESERVED_DEVICE_NAMES.has(baseName) ? baseName : undefined;\n}\n\nexport function getReservedDeviceNameForPath(requestedPath: string): string | undefined {\n const segments = requestedPath.split(/[\\\\/]/u);\n for (const segment of segments) {\n const reserved = getReservedDeviceName(segment);\n if (reserved) {\n return reserved;\n }\n }\n return undefined;\n}\n\nexport function isWindowsDriveRelativePath(requestedPath: string): boolean {\n // Check on all platforms so cross-platform clients cannot smuggle drive-relative\n // inputs (e.g. C:relative) to a POSIX-hosted server where path.resolve would\n // silently expand them relative to CWD.\n if (requestedPath.length < 2) {\n return false;\n }\n if (requestedPath.charCodeAt(1) !== CHAR_COLON) {\n return false;\n }\n if (!isAlpha(requestedPath.charCodeAt(0))) {\n return false;\n }\n\n if (requestedPath.length === 2) {\n return true;\n }\n return !isSlash(requestedPath.charCodeAt(2));\n}\n\nfunction expandHome(filepath: string): string {\n if (filepath === '~' || filepath.startsWith('~/') || filepath.startsWith('~\\\\')) {\n const rest = filepath.slice(1).replace(/^[/\\\\]+/, '');\n return rest ? join(HOMEDIR, rest) : HOMEDIR;\n }\n return filepath;\n}\n\nexport function normalizePath(p: string): string {\n const resolved = resolve(expandHome(p));\n\n // On Windows only the drive letter is lowercased (e.g. \"C:\\Foo\\Bar\").\n // The rest of the path retains its original casing.\n // IMPORTANT: callers must use isSamePath / isPathInsideDirectory for all\n // equality and containment checks — never raw string equality — because\n // those helpers apply full case-folding before comparing.\n if (IS_WINDOWS && resolved.length >= 2 && resolved.charCodeAt(1) === CHAR_COLON) {\n return resolved.charAt(0).toLowerCase() + resolved.slice(1);\n }\n\n return resolved;\n}\n\nconst IS_CASE_INSENSITIVE_FS = IS_WINDOWS || platform() === 'darwin';\n\nexport function normalizeCaseForComparison(value: string): string {\n return IS_CASE_INSENSITIVE_FS ? value.toLowerCase() : value;\n}\n\nexport function isSamePath(left: string, right: string): boolean {\n if (left === right) {\n return true;\n }\n const leftResolved = normalizeCaseForComparison(resolve(left));\n const rightResolved = normalizeCaseForComparison(resolve(right));\n return leftResolved === rightResolved;\n}\n\nexport function normalizeAllowedDirectory(dir: string): string {\n const trimmed = dir.trim();\n if (trimmed.length === 0) return '';\n\n const normalized = normalizePath(trimmed);\n const { root } = parse(normalized);\n\n // Keep filesystem roots as-is (\"/\", \"c:\\\\\", \"\\\\\\\\server\\\\share\\\\\").\n if (isSamePath(normalized, root)) {\n return root;\n }\n\n return normalized.length > 1 && normalized.endsWith(sep) ? normalized.slice(0, -1) : normalized;\n}\n\nexport function isPathInsideDirectory(\n normalizedDirectory: string,\n normalizedCandidate: string,\n): boolean {\n const root = normalizeCaseForComparison(normalizedDirectory);\n const candidate = normalizeCaseForComparison(normalizedCandidate);\n\n if (root === candidate) return true;\n if (!candidate.startsWith(root)) return false;\n\n if (isSlash(root.charCodeAt(root.length - 1))) return true;\n return isSlash(candidate.charCodeAt(root.length));\n}\n\nexport function isPathWithinDirectories(\n normalizedPath: string,\n allowedDirs: readonly string[],\n): boolean {\n return allowedDirs.some((dir) => isPathInsideDirectory(dir, normalizedPath));\n}\n"]}
@@ -76,16 +76,14 @@ export declare class PathGuard {
76
76
  */
77
77
  isEntryAccessible(entryPath: string): Promise<boolean>;
78
78
  validateExistingPath(requestedPath: string): Promise<ValidatedPath>;
79
- /** True when a normalized path equals its own filesystem root (`C:\`, `/`). */
80
- private isFilesystemRoot;
81
79
  /**
82
- * True when a grant must never admit `targetDir` — a bare filesystem root, or
83
- * an unsafe path (home, /etc, C:\Windows, ...). Checked on the lexical path
84
- * AND on the one it resolves to: `expandAllowedDirectories` pushes each root's
85
- * realpath into the allowed set, so a lexical-only check let a symlink or
86
- * junction aliasing $HOME/C:\Windows in under an innocuous name — and the
87
- * confirmation prompt showed the alias, not the target. A target that cannot
88
- * be resolved is judged lexically; it does not exist to escape into.
80
+ * True when a grant must never admit `targetDir` — an unsafe path (home,
81
+ * /etc, C:\Windows, ...). Checked on the lexical path AND on the one it
82
+ * resolves to: `resolveAllowedDirectoriesState` pushes each root's realpath
83
+ * into the allowed set, so a lexical-only check let a symlink or junction
84
+ * aliasing $HOME/C:\Windows in under an innocuous name — and the confirmation
85
+ * prompt showed the alias, not the target. A target that cannot be resolved
86
+ * is judged lexically; it does not exist to escape into.
89
87
  */
90
88
  private isUnsafeGrantTarget;
91
89
  /** Walk up from a blocked path to the closest existing ancestor directory. */
@@ -1 +1 @@
1
- {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/core/path.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"path.d.ts","sourceRoot":"","sources":["../../src/core/path.ts"],"names":[],"mappings":"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"}
package/dist/core/path.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { lstat, readlink, realpath, stat } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
- import { basename, dirname, isAbsolute, join, parse, relative, resolve } from 'node:path';
4
- import { timedSignal, withAbort } from './concurrency.js';
3
+ import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { withAbort } from './concurrency.js';
5
5
  import { cli } from './config.js';
6
6
  import { ERRNO_MAP, ErrorCode, FsError, isFsError, isNodeError, isNotFoundErrno, rethrowIfAborted, SKIPPABLE_ERRNOS, SKIPPABLE_FS_CODES, } from './errors.js';
7
7
  import { Logger } from './observability.js';
@@ -11,7 +11,7 @@ import { parseTrueEnvFlag, toPosixPath } from './primitives.js';
11
11
  import { SensitiveMatcher } from './sensitive.js';
12
12
  import { ROOTS_TIMEOUT_MS } from './util.js';
13
13
  /** True when `normalizedRoot` really resolves inside `bounds` (FS_ROOT_BOUNDARY). */
14
- async function isRootWithin(normalizedRoot, bounds, label, signal) {
14
+ async function isRootWithin(normalizedRoot, bounds, signal) {
15
15
  try {
16
16
  signal?.throwIfAborted();
17
17
  const realPath = await withAbort(realpath(normalizedRoot), signal);
@@ -22,20 +22,17 @@ async function isRootWithin(normalizedRoot, bounds, label, signal) {
22
22
  if (isNotFoundErrno(error)) {
23
23
  return false;
24
24
  }
25
- Logger.warn(`${label}: realpath failed unexpectedly`, {
25
+ Logger.warn('grantBoundary: realpath failed unexpectedly', {
26
26
  root: normalizedRoot,
27
27
  error: String(error),
28
28
  });
29
29
  return false;
30
30
  }
31
31
  }
32
- async function filterRootsWithin(roots, bounds, label, signal) {
32
+ async function filterRootsWithin(roots, bounds, signal) {
33
33
  const normalizedBounds = normalizeAllowedDirectories(bounds);
34
34
  const normalizedRoots = roots.map(normalizePath);
35
- if (normalizedRoots.length === 0) {
36
- return [];
37
- }
38
- const results = await Promise.all(normalizedRoots.map((root) => isRootWithin(root, normalizedBounds, label, signal)));
35
+ const results = await Promise.all(normalizedRoots.map((root) => isRootWithin(root, normalizedBounds, signal)));
39
36
  return normalizedRoots.filter((_, i) => results[i]);
40
37
  }
41
38
  /** `path.relative` with forward slashes, so displayed paths match across platforms. */
@@ -72,25 +69,15 @@ export async function resolveRealPath(normalized, signal) {
72
69
  throw error;
73
70
  }
74
71
  }
75
- async function expandAllowedDirectories(primaryDirs, signal) {
76
- const realPaths = await Promise.all(primaryDirs.map((dir) => resolveRealPath(dir, signal)));
77
- const expanded = [];
78
- for (let i = 0; i < primaryDirs.length; i++) {
79
- const primary = primaryDirs[i];
80
- if (!primary) {
81
- continue;
82
- }
83
- expanded.push(primary);
84
- const real = realPaths[i];
85
- if (real && !isSamePath(real, primary)) {
86
- expanded.push(real);
87
- }
88
- }
89
- return [...new Set(expanded)];
90
- }
91
72
  export async function resolveAllowedDirectoriesState(dirs, signal) {
92
73
  const primary = normalizeAllowedDirectories(dirs);
93
- return expandAllowedDirectories(primary, signal);
74
+ const reals = await Promise.all(primary.map((dir) => resolveRealPath(dir, signal)));
75
+ return [
76
+ ...new Set(primary.flatMap((dir, i) => {
77
+ const real = reals[i];
78
+ return real && !isSamePath(real, dir) ? [dir, real] : [dir];
79
+ })),
80
+ ];
94
81
  }
95
82
  // ---------------------------------------------------------------------------
96
83
  // Windows helpers
@@ -193,23 +180,18 @@ export class PathGuard {
193
180
  const details = await this.validateExistingPathDetailed(requestedPath);
194
181
  return details.resolvedPath;
195
182
  }
196
- /** True when a normalized path equals its own filesystem root (`C:\`, `/`). */
197
- isFilesystemRoot(normalizedPath) {
198
- return isSamePath(normalizedPath, parse(normalizedPath).root);
199
- }
200
183
  /**
201
- * True when a grant must never admit `targetDir` — a bare filesystem root, or
202
- * an unsafe path (home, /etc, C:\Windows, ...). Checked on the lexical path
203
- * AND on the one it resolves to: `expandAllowedDirectories` pushes each root's
204
- * realpath into the allowed set, so a lexical-only check let a symlink or
205
- * junction aliasing $HOME/C:\Windows in under an innocuous name — and the
206
- * confirmation prompt showed the alias, not the target. A target that cannot
207
- * be resolved is judged lexically; it does not exist to escape into.
184
+ * True when a grant must never admit `targetDir` — an unsafe path (home,
185
+ * /etc, C:\Windows, ...). Checked on the lexical path AND on the one it
186
+ * resolves to: `resolveAllowedDirectoriesState` pushes each root's realpath
187
+ * into the allowed set, so a lexical-only check let a symlink or junction
188
+ * aliasing $HOME/C:\Windows in under an innocuous name — and the confirmation
189
+ * prompt showed the alias, not the target. A target that cannot be resolved
190
+ * is judged lexically; it does not exist to escape into.
208
191
  */
209
192
  async isUnsafeGrantTarget(targetDir) {
210
- const isRefused = (dir) => this.isFilesystemRoot(dir) || isUnsafeCwdPath(dir);
211
193
  const normalized = normalizePath(targetDir);
212
- if (isRefused(normalized))
194
+ if (isUnsafeCwdPath(normalized))
213
195
  return true;
214
196
  let resolved;
215
197
  try {
@@ -218,7 +200,7 @@ export class PathGuard {
218
200
  catch {
219
201
  return false;
220
202
  }
221
- return !isSamePath(resolved, normalized) && isRefused(resolved);
203
+ return !isSamePath(resolved, normalized) && isUnsafeCwdPath(resolved);
222
204
  }
223
205
  /** Walk up from a blocked path to the closest existing ancestor directory. */
224
206
  async resolveGrantTargetDir(blockedPath) {
@@ -618,11 +600,11 @@ export class PathGuard {
618
600
  }
619
601
  }
620
602
  const baseline = [...cliAllowedDirs, ...envAllowedDirs, ...allowCwdDirs];
621
- const signal = timedSignal(undefined, ROOTS_TIMEOUT_MS);
603
+ const signal = AbortSignal.timeout(ROOTS_TIMEOUT_MS);
622
604
  // FS_ROOT_BOUNDARY is the only filter grants answer to (see
623
605
  // `grantedDirectories`); without one they pass through as accepted.
624
606
  const grantsToInclude = boundaries.length > 0
625
- ? await filterRootsWithin(this.grantedDirectories, boundaries, 'grantBoundary', signal)
607
+ ? await filterRootsWithin(this.grantedDirectories, boundaries, signal)
626
608
  : this.grantedDirectories;
627
609
  const combined = [...baseline, ...grantsToInclude];
628
610
  const nextState = await resolveAllowedDirectoriesState(combined, signal);