@j0hanz/filesystem-mcp 2.1.8 → 2.3.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 (149) hide show
  1. package/README.md +41 -29
  2. package/dist/cli-help.d.ts.map +1 -1
  3. package/dist/cli-help.js +10 -2
  4. package/dist/cli-help.js.map +1 -1
  5. package/dist/cli.d.ts.map +1 -1
  6. package/dist/cli.js +10 -5
  7. package/dist/cli.js.map +1 -1
  8. package/dist/core/concurrency.d.ts.map +1 -1
  9. package/dist/core/concurrency.js +21 -6
  10. package/dist/core/concurrency.js.map +1 -1
  11. package/dist/core/config.d.ts +2 -0
  12. package/dist/core/config.d.ts.map +1 -1
  13. package/dist/core/config.js.map +1 -1
  14. package/dist/core/cursor.d.ts +11 -0
  15. package/dist/core/cursor.d.ts.map +1 -1
  16. package/dist/core/cursor.js +10 -0
  17. package/dist/core/cursor.js.map +1 -1
  18. package/dist/core/errors.d.ts +6 -9
  19. package/dist/core/errors.d.ts.map +1 -1
  20. package/dist/core/errors.js +58 -102
  21. package/dist/core/errors.js.map +1 -1
  22. package/dist/core/file-uri.d.ts +5 -1
  23. package/dist/core/file-uri.d.ts.map +1 -1
  24. package/dist/core/file-uri.js +6 -2
  25. package/dist/core/file-uri.js.map +1 -1
  26. package/dist/core/fmt.d.ts +0 -1
  27. package/dist/core/fmt.d.ts.map +1 -1
  28. package/dist/core/fmt.js +0 -1
  29. package/dist/core/fmt.js.map +1 -1
  30. package/dist/core/fs.d.ts +31 -1
  31. package/dist/core/fs.d.ts.map +1 -1
  32. package/dist/core/fs.js +142 -2
  33. package/dist/core/fs.js.map +1 -1
  34. package/dist/core/glob.d.ts +4 -13
  35. package/dist/core/glob.d.ts.map +1 -1
  36. package/dist/core/glob.js +14 -14
  37. package/dist/core/glob.js.map +1 -1
  38. package/dist/core/input-required.d.ts +16 -17
  39. package/dist/core/input-required.d.ts.map +1 -1
  40. package/dist/core/input-required.js +42 -47
  41. package/dist/core/input-required.js.map +1 -1
  42. package/dist/core/observability.d.ts +9 -0
  43. package/dist/core/observability.d.ts.map +1 -1
  44. package/dist/core/observability.js +13 -1
  45. package/dist/core/observability.js.map +1 -1
  46. package/dist/core/page-store.d.ts +4 -3
  47. package/dist/core/page-store.d.ts.map +1 -1
  48. package/dist/core/page-store.js +4 -3
  49. package/dist/core/page-store.js.map +1 -1
  50. package/dist/core/primitives.d.ts +7 -0
  51. package/dist/core/primitives.d.ts.map +1 -1
  52. package/dist/core/primitives.js +15 -8
  53. package/dist/core/primitives.js.map +1 -1
  54. package/dist/core/search.d.ts +8 -4
  55. package/dist/core/search.d.ts.map +1 -1
  56. package/dist/core/search.js +23 -24
  57. package/dist/core/search.js.map +1 -1
  58. package/dist/core/sensitive.d.ts +4 -2
  59. package/dist/core/sensitive.d.ts.map +1 -1
  60. package/dist/core/sensitive.js +261 -43
  61. package/dist/core/sensitive.js.map +1 -1
  62. package/dist/core/util.d.ts.map +1 -1
  63. package/dist/core/util.js +2 -11
  64. package/dist/core/util.js.map +1 -1
  65. package/dist/core/watcher-registry.d.ts.map +1 -1
  66. package/dist/core/watcher-registry.js +5 -1
  67. package/dist/core/watcher-registry.js.map +1 -1
  68. package/dist/instructions.js +11 -11
  69. package/dist/instructions.js.map +1 -1
  70. package/dist/resources.d.ts.map +1 -1
  71. package/dist/resources.js +13 -4
  72. package/dist/resources.js.map +1 -1
  73. package/dist/server.d.ts +1 -9
  74. package/dist/server.d.ts.map +1 -1
  75. package/dist/server.js +22 -27
  76. package/dist/server.js.map +1 -1
  77. package/dist/tools/create.d.ts.map +1 -1
  78. package/dist/tools/create.js +163 -14
  79. package/dist/tools/create.js.map +1 -1
  80. package/dist/tools/define.d.ts +6 -1
  81. package/dist/tools/define.d.ts.map +1 -1
  82. package/dist/tools/define.js +29 -9
  83. package/dist/tools/define.js.map +1 -1
  84. package/dist/tools/delete.d.ts +2 -0
  85. package/dist/tools/delete.d.ts.map +1 -0
  86. package/dist/tools/{delete-file.js → delete.js} +12 -15
  87. package/dist/tools/delete.js.map +1 -0
  88. package/dist/tools/edit.d.ts.map +1 -1
  89. package/dist/tools/edit.js +77 -69
  90. package/dist/tools/edit.js.map +1 -1
  91. package/dist/tools/find-files.d.ts +2 -0
  92. package/dist/tools/find-files.d.ts.map +1 -0
  93. package/dist/tools/{search-files.js → find-files.js} +14 -19
  94. package/dist/tools/find-files.js.map +1 -0
  95. package/dist/tools/index.d.ts +7 -6
  96. package/dist/tools/index.d.ts.map +1 -1
  97. package/dist/tools/index.js +16 -15
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/list-roots.d.ts +2 -0
  100. package/dist/tools/list-roots.d.ts.map +1 -0
  101. package/dist/tools/{roots.js → list-roots.js} +2 -2
  102. package/dist/tools/list-roots.js.map +1 -0
  103. package/dist/tools/list.d.ts.map +1 -1
  104. package/dist/tools/list.js +28 -32
  105. package/dist/tools/list.js.map +1 -1
  106. package/dist/tools/move.d.ts.map +1 -1
  107. package/dist/tools/move.js +17 -36
  108. package/dist/tools/move.js.map +1 -1
  109. package/dist/tools/patch.js +3 -3
  110. package/dist/tools/patch.js.map +1 -1
  111. package/dist/tools/read.d.ts +1 -1
  112. package/dist/tools/read.d.ts.map +1 -1
  113. package/dist/tools/read.js +1 -1
  114. package/dist/tools/read.js.map +1 -1
  115. package/dist/tools/replace-text.d.ts +2 -0
  116. package/dist/tools/replace-text.d.ts.map +1 -0
  117. package/dist/tools/{replace-in-files.js → replace-text.js} +5 -5
  118. package/dist/tools/replace-text.js.map +1 -0
  119. package/dist/tools/search-text.d.ts +2 -0
  120. package/dist/tools/search-text.d.ts.map +1 -0
  121. package/dist/tools/{search-content.js → search-text.js} +71 -13
  122. package/dist/tools/search-text.js.map +1 -0
  123. package/dist/tools/stat.d.ts +1 -1
  124. package/dist/tools/stat.d.ts.map +1 -1
  125. package/dist/tools/stat.js +1 -1
  126. package/dist/tools/stat.js.map +1 -1
  127. package/dist/transport/http.d.ts.map +1 -1
  128. package/dist/transport/http.js +5 -1
  129. package/dist/transport/http.js.map +1 -1
  130. package/dist/transport/stdio.d.ts +1 -0
  131. package/dist/transport/stdio.d.ts.map +1 -1
  132. package/dist/transport/stdio.js +1 -0
  133. package/dist/transport/stdio.js.map +1 -1
  134. package/package.json +6 -9
  135. package/dist/tools/delete-file.d.ts +0 -2
  136. package/dist/tools/delete-file.d.ts.map +0 -1
  137. package/dist/tools/delete-file.js.map +0 -1
  138. package/dist/tools/replace-in-files.d.ts +0 -2
  139. package/dist/tools/replace-in-files.d.ts.map +0 -1
  140. package/dist/tools/replace-in-files.js.map +0 -1
  141. package/dist/tools/roots.d.ts +0 -2
  142. package/dist/tools/roots.d.ts.map +0 -1
  143. package/dist/tools/roots.js.map +0 -1
  144. package/dist/tools/search-content.d.ts +0 -2
  145. package/dist/tools/search-content.d.ts.map +0 -1
  146. package/dist/tools/search-content.js.map +0 -1
  147. package/dist/tools/search-files.d.ts +0 -2
  148. package/dist/tools/search-files.d.ts.map +0 -1
  149. package/dist/tools/search-files.js.map +0 -1
@@ -42,8 +42,8 @@ export const requestStateCodec = {
42
42
  };
43
43
  /**
44
44
  * Build a single-select enum confirmation input. The form renders a `choice`
45
- * field whose options are the titled `choices`; the caller reads the selection
46
- * with `readAcceptedChoice`.
45
+ * field whose options are the offered `choices`; the caller reads the
46
+ * selection with `readAcceptedChoice`.
47
47
  */
48
48
  export function choiceInput(key, message, choices) {
49
49
  return { key, message, choices };
@@ -62,6 +62,29 @@ export function multiSelectInput(key, message, choices) {
62
62
  multi: true,
63
63
  };
64
64
  }
65
+ // Response shapes handed to the SDK's schema-aware `acceptedContent` overload:
66
+ // it returns `undefined` for a missing key, a decline/cancel, a non-elicit
67
+ // response, AND a payload that fails validation — one call covers every refusal
68
+ // case the readers below used to hand-check. `buildInputRequired` reuses
69
+ // `ConfirmContent` as the no-choices form schema (below), so these sit above it.
70
+ const ConfirmContent = z.object({ confirm: z.boolean() });
71
+ const ChoiceContent = z.object({ choice: z.string() });
72
+ const MultiChoiceContent = z.object({ choice: z.array(z.string()) });
73
+ /**
74
+ * The form the client renders for one pending input, as the Zod schema the
75
+ * SDK converts to the elicitation wire shape. A boolean `confirm` when no
76
+ * choices are offered; otherwise a `choice` enum over the offered values,
77
+ * wrapped in an array for multi-select so the client may accept a subset.
78
+ * The offered lists are never empty by construction (single-select always
79
+ * offers two literals; multi-select only runs with two or more grant dirs),
80
+ * so no empty-array guard is added.
81
+ */
82
+ function requestedSchemaFor(input) {
83
+ if (!input.choices)
84
+ return ConfirmContent;
85
+ const value = z.enum(input.choices);
86
+ return z.object({ choice: input.multi ? z.array(value) : value });
87
+ }
65
88
  /**
66
89
  * Build an `input_required` result carrying one boolean confirmation per pending
67
90
  * item, plus the integrity-protected `requestState` sealing the operation kind
@@ -70,40 +93,9 @@ export function multiSelectInput(key, message, choices) {
70
93
  export async function buildInputRequired(pending, inputs) {
71
94
  const inputRequests = {};
72
95
  for (const input of inputs) {
73
- const requestedSchema = input.choices
74
- ? input.multi
75
- ? {
76
- // Multi-select enum (matches MultiSelectEnumSchema): `choice` is a
77
- // string array; the client may accept a subset of the offered dirs.
78
- type: 'object',
79
- properties: {
80
- choice: {
81
- type: 'array',
82
- items: {
83
- anyOf: input.choices.map((c) => ({ const: c.value, title: c.title })),
84
- },
85
- },
86
- },
87
- required: ['choice'],
88
- }
89
- : {
90
- type: 'object',
91
- properties: {
92
- choice: {
93
- type: 'string',
94
- oneOf: input.choices.map((c) => ({ const: c.value, title: c.title })),
95
- },
96
- },
97
- required: ['choice'],
98
- }
99
- : {
100
- type: 'object',
101
- properties: { confirm: { type: 'boolean', title: 'Confirm' } },
102
- required: ['confirm'],
103
- };
104
96
  inputRequests[input.key] = inputRequired.elicit({
105
97
  message: input.message,
106
- requestedSchema,
98
+ requestedSchema: requestedSchemaFor(input),
107
99
  });
108
100
  }
109
101
  const requestState = await requestStateCodec.mint({
@@ -124,6 +116,8 @@ const NO_ELICITATION_HINT = {
124
116
  'Delete the destination first, or move to a path that does not exist yet.',
125
117
  copy: 'Overwriting an existing destination needs a confirmation this client cannot show. ' +
126
118
  'Pass overwrite=true to replace it without confirming, or copy to a path that does not exist yet.',
119
+ create: 'Overwriting an existing file needs a confirmation this client cannot show. ' +
120
+ 'Pass overwrite: true on that entry to replace it without confirming, append: true to add to it, or read it and use edit.',
127
121
  grant: 'Granting access to a directory outside the allowed roots needs a confirmation this client cannot show. ' +
128
122
  'Call list_roots and use a path under one of the roots it returns.',
129
123
  };
@@ -165,13 +159,16 @@ export async function pendingRoundTrip(opts) {
165
159
  }
166
160
  return undefined;
167
161
  }
168
- // Response shapes handed to the SDK's schema-aware `acceptedContent` overload:
169
- // it returns `undefined` for a missing key, a decline/cancel, a non-elicit
170
- // response, AND a payload that fails validation — one call covers every refusal
171
- // case the readers below used to hand-check.
172
- const ConfirmContent = z.object({ confirm: z.boolean() });
173
- const ChoiceContent = z.object({ choice: z.string() });
174
- const MultiChoiceContent = z.object({ choice: z.array(z.string()) });
162
+ /**
163
+ * Round-trip key for `pendingSorted[index]` — the single home of the
164
+ * `confirm_${i}` convention. The build side mints it with the map index; the
165
+ * read side re-derives it from the same sorted pending set the requestState
166
+ * binds (R9), so a format change lands here once instead of at six call
167
+ * sites.
168
+ */
169
+ export function confirmKey(index) {
170
+ return `confirm_${index}`;
171
+ }
175
172
  /**
176
173
  * Read one pending item's boolean confirmation from a retried request's
177
174
  * `inputResponses`. Returns `true` only when the client explicitly accepted AND
@@ -220,11 +217,9 @@ export function readAcceptedMultiChoice(responses, key) {
220
217
  * it without allocating.
221
218
  */
222
219
  function pathsEqual(a, b) {
223
- if (a.length !== b.length)
224
- return false;
225
- for (let i = 0; i < a.length; i++)
226
- if (a[i] !== b[i])
227
- return false;
228
- return true;
220
+ // The length check is load-bearing, not an early-out: `every` alone returns
221
+ // true whenever `a` is a prefix of a longer `b`, so a confirmation minted for
222
+ // ['/x'] would authorize a retry for ['/x', '/y'] — a direct R9 bypass.
223
+ return a.length === b.length && a.every((value, index) => value === b[index]);
229
224
  }
230
225
  //# sourceMappingURL=input-required.js.map
@@ -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. */\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 * 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' | 'create' | '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 single-select enum (`choice` field) over\n * these values instead of a boolean `confirm`.\n */\n readonly choices?: readonly 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 offered `choices`; the caller reads the\n * selection with `readAcceptedChoice`.\n */\nexport function choiceInput(\n key: string,\n message: string,\n choices: readonly 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 string[],\n): PendingInput {\n return {\n key,\n message,\n choices,\n multi: true,\n };\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. `buildInputRequired` reuses\n// `ConfirmContent` as the no-choices form schema (below), so these sit above it.\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 * The form the client renders for one pending input, as the Zod schema the\n * SDK converts to the elicitation wire shape. A boolean `confirm` when no\n * choices are offered; otherwise a `choice` enum over the offered values,\n * wrapped in an array for multi-select so the client may accept a subset.\n * The offered lists are never empty by construction (single-select always\n * offers two literals; multi-select only runs with two or more grant dirs),\n * so no empty-array guard is added.\n */\nfunction requestedSchemaFor(input: PendingInput): z.ZodObject {\n if (!input.choices) return ConfirmContent;\n const value = z.enum(input.choices);\n return z.object({ choice: input.multi ? z.array(value) : value });\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 inputRequests[input.key] = inputRequired.elicit({\n message: input.message,\n requestedSchema: requestedSchemaFor(input),\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 create:\n 'Overwriting an existing file needs a confirmation this client cannot show. ' +\n 'Pass overwrite: true on that entry to replace it without confirming, append: true to add to it, or read it and use edit.',\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/**\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 // The length check is load-bearing, not an early-out: `every` alone returns\n // true whenever `a` is a prefix of a longer `b`, so a confirmation minted for\n // ['/x'] would authorize a retry for ['/x', '/y'] — a direct R9 bypass.\n return a.length === b.length && a.every((value, index) => value === b[index]);\n}\n"]}
@@ -1,4 +1,13 @@
1
1
  export type LoggingLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical' | 'alert' | 'emergency';
2
+ /**
3
+ * A client-supplied value (a JSON-RPC id, a `traceparent`, a tool's own log
4
+ * message) made safe for one stderr line: a control character or a Unicode
5
+ * line/paragraph separator (U+2028/U+2029, which `\p{Cc}` misses) would let
6
+ * a caller forge extra log lines, and an unbounded value would let it flood
7
+ * one. `max` bounds the value; pass `Infinity` for free text that is only
8
+ * flattened.
9
+ */
10
+ export declare function sanitizeLogField(value: string, max?: number): string;
2
11
  export declare const Logger: {
3
12
  emit: (level: LoggingLevel, message: string) => void;
4
13
  info: (message: string, ...args: unknown[]) => void;
@@ -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;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,SAAM,GAAG,MAAM,CAEjE;AAED,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
@@ -53,6 +54,17 @@ function write(level, message, args) {
53
54
  const prefix = `[${level}]`;
54
55
  console.error(`${prefix} ${message}`, ...args);
55
56
  }
57
+ /**
58
+ * A client-supplied value (a JSON-RPC id, a `traceparent`, a tool's own log
59
+ * message) made safe for one stderr line: a control character or a Unicode
60
+ * line/paragraph separator (U+2028/U+2029, which `\p{Cc}` misses) would let
61
+ * a caller forge extra log lines, and an unbounded value would let it flood
62
+ * one. `max` bounds the value; pass `Infinity` for free text that is only
63
+ * flattened.
64
+ */
65
+ export function sanitizeLogField(value, max = 128) {
66
+ return value.replace(/[\p{Cc}\p{Zl}\p{Zp}]+/gu, ' ').slice(0, max);
67
+ }
56
68
  export const Logger = {
57
69
  emit: (level, message) => {
58
70
  write(level, message, []);
@@ -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\n/**\n * A client-supplied value (a JSON-RPC id, a `traceparent`, a tool's own log\n * message) made safe for one stderr line: a control character or a Unicode\n * line/paragraph separator (U+2028/U+2029, which `\\p{Cc}` misses) would let\n * a caller forge extra log lines, and an unbounded value would let it flood\n * one. `max` bounds the value; pass `Infinity` for free text that is only\n * flattened.\n */\nexport function sanitizeLogField(value: string, max = 128): string {\n return value.replace(/[\\p{Cc}\\p{Zl}\\p{Zp}]+/gu, ' ').slice(0, max);\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"]}
@@ -19,9 +19,10 @@ export declare function invalidCursor(): FsError;
19
19
  * slice a stored array instead of re-scanning and re-sorting the filesystem.
20
20
  *
21
21
  * ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000
22
- * `list` entries or 32 x 10,000 `search_text` matches, order of a few hundred MB
23
- * held for 60s, and the HTTP leg shares one store so any caller can drive it.
24
- * If that shows up as memory pressure, meter bytes the way `ResourceStore` does.
22
+ * `list` entries or 32 x 10,000 `search_text` matches, each match retaining up
23
+ * to 20 more line strings at `context: 10`, order of a few hundred MB held for
24
+ * 60s, and the HTTP leg shares one store so any caller can drive it. If that
25
+ * shows up as memory pressure, meter bytes the way `ResourceStore` does.
25
26
  */
26
27
  export declare class PageSnapshotStore {
27
28
  private readonly byId;
@@ -1 +1 @@
1
- {"version":3,"file":"page-store.d.ts","sourceRoot":"","sources":["../../src/core/page-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,aAAa,CAAC;AAYjD,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAED;;;;;;;;GAQG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,GAAE,wBAA6B;IAMlD,OAAO,CAAC,YAAY;IAOpB,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM;IAmB3F,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IAYhF,KAAK,IAAI,IAAI;CAGd"}
1
+ {"version":3,"file":"page-store.d.ts","sourceRoot":"","sources":["../../src/core/page-store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAa,OAAO,EAAE,MAAM,aAAa,CAAC;AAYjD,MAAM,WAAW,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,CAAC,GAAG,OAAO;IACpD,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,OAAO,CAKvC;AAED;;;;;;;;;GASG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAyC;IAC9D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;gBAEvB,OAAO,GAAE,wBAA6B;IAMlD,OAAO,CAAC,YAAY;IAOpB,MAAM,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,SAAS,OAAO,EAAE,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,MAAM;IAmB3F,IAAI,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC;IAYhF,KAAK,IAAI,IAAI;CAGd"}
@@ -15,9 +15,10 @@ export function invalidCursor() {
15
15
  * slice a stored array instead of re-scanning and re-sorting the filesystem.
16
16
  *
17
17
  * ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000
18
- * `list` entries or 32 x 10,000 `search_text` matches, order of a few hundred MB
19
- * held for 60s, and the HTTP leg shares one store so any caller can drive it.
20
- * If that shows up as memory pressure, meter bytes the way `ResourceStore` does.
18
+ * `list` entries or 32 x 10,000 `search_text` matches, each match retaining up
19
+ * to 20 more line strings at `context: 10`, order of a few hundred MB held for
20
+ * 60s, and the HTTP leg shares one store so any caller can drive it. If that
21
+ * shows up as memory pressure, meter bytes the way `ResourceStore` does.
21
22
  */
22
23
  export class PageSnapshotStore {
23
24
  byId = new Map();
@@ -1 +1 @@
1
- {"version":3,"file":"page-store.js","sourceRoot":"","sources":["../../src/core/page-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,qBAAqB,GAAG,EAAE,CAAC;AACjC,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,CAAC;AAoBjC;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,OAAO,CAChB,SAAS,CAAC,aAAa,EACvB,0DAA0D,CAC3D,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,OAAO,iBAAiB;IACX,IAAI,GAAG,IAAI,GAAG,EAA8B,CAAC;IAC7C,YAAY,CAAS;IACrB,KAAK,CAAS;IACd,GAAG,CAAe;IAEnC,YAAY,UAAoC,EAAE;QAChD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAClE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;QAC7C,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACrC,CAAC;IAEO,YAAY;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,MAAM,CAAC,MAA2E;QAChF,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE;YACxB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK;SACnC,CAAC,CAAC;QACH,wEAAwE;QACxE,iDAAiD;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM;YACvB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,CAAmB,UAAkB,EAAE,QAAgB;QACzD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,QAAQ,KAAK,QAAQ;YAAE,MAAM,aAAa,EAAE,CAAC;QACxD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACjC,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAqB;YAClC,QAAQ,EAAE,KAAK,CAAC,QAAa;SAC9B,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;CACF","sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { ErrorCode, FsError } from './errors.js';\n\nconst DEFAULT_MAX_SNAPSHOTS = 32;\nconst DEFAULT_TTL_MS = 60 * 1000;\n\ninterface StoredPageSnapshot {\n readonly queryKey: string;\n readonly items: readonly unknown[];\n readonly metadata: unknown;\n readonly expiresAt: number;\n}\n\nexport interface PageSnapshot<T = unknown, M = unknown> {\n readonly items: readonly T[];\n readonly metadata: M;\n}\n\nexport interface PageSnapshotStoreOptions {\n readonly maxSnapshots?: number;\n readonly ttlMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * The one cursor rejection: a cursor whose snapshot expired, was evicted, was\n * already consumed past its end, or belongs to a different query. Every case\n * has the same remedy, so they share one message.\n */\nexport function invalidCursor(): FsError {\n return new FsError(\n ErrorCode.INVALID_INPUT,\n 'Invalid cursor. Request the first page without a cursor.',\n );\n}\n\n/**\n * Short-lived snapshots of a completed query's full result set, so later pages\n * slice a stored array instead of re-scanning and re-sorting the filesystem.\n *\n * ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000\n * `list` entries or 32 x 10,000 `search_text` matches, order of a few hundred MB\n * held for 60s, and the HTTP leg shares one store so any caller can drive it.\n * If that shows up as memory pressure, meter bytes the way `ResourceStore` does.\n */\nexport class PageSnapshotStore {\n private readonly byId = new Map<string, StoredPageSnapshot>();\n private readonly maxSnapshots: number;\n private readonly ttlMs: number;\n private readonly now: () => number;\n\n constructor(options: PageSnapshotStoreOptions = {}) {\n this.maxSnapshots = options.maxSnapshots ?? DEFAULT_MAX_SNAPSHOTS;\n this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;\n this.now = options.now ?? Date.now;\n }\n\n private pruneExpired(): void {\n const now = this.now();\n for (const [snapshotId, entry] of this.byId) {\n if (entry.expiresAt <= now) this.byId.delete(snapshotId);\n }\n }\n\n create(params: { queryKey: string; items: readonly unknown[]; metadata?: unknown }): string {\n this.pruneExpired();\n const snapshotId = randomUUID();\n this.byId.set(snapshotId, {\n queryKey: params.queryKey,\n items: params.items,\n metadata: params.metadata,\n expiresAt: this.now() + this.ttlMs,\n });\n // Oldest first: Map keeps insertion order and `read` re-inserts, so the\n // first key is the least recently used snapshot.\n while (this.byId.size > this.maxSnapshots) {\n const oldest = this.byId.keys().next();\n if (oldest.done) break;\n this.byId.delete(oldest.value);\n }\n return snapshotId;\n }\n\n read<T, M = undefined>(snapshotId: string, queryKey: string): PageSnapshot<T, M> {\n this.pruneExpired();\n const entry = this.byId.get(snapshotId);\n if (entry?.queryKey !== queryKey) throw invalidCursor();\n this.byId.delete(snapshotId);\n this.byId.set(snapshotId, entry);\n return {\n items: entry.items as readonly T[],\n metadata: entry.metadata as M,\n };\n }\n\n clear(): void {\n this.byId.clear();\n }\n}\n"]}
1
+ {"version":3,"file":"page-store.js","sourceRoot":"","sources":["../../src/core/page-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,qBAAqB,GAAG,EAAE,CAAC;AACjC,MAAM,cAAc,GAAG,EAAE,GAAG,IAAI,CAAC;AAoBjC;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,OAAO,CAChB,SAAS,CAAC,aAAa,EACvB,0DAA0D,CAC3D,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,iBAAiB;IACX,IAAI,GAAG,IAAI,GAAG,EAA8B,CAAC;IAC7C,YAAY,CAAS;IACrB,KAAK,CAAS;IACd,GAAG,CAAe;IAEnC,YAAY,UAAoC,EAAE;QAChD,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAClE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,cAAc,CAAC;QAC7C,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACrC,CAAC;IAEO,YAAY;QAClB,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAC5C,IAAI,KAAK,CAAC,SAAS,IAAI,GAAG;gBAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,MAAM,CAAC,MAA2E;QAChF,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,UAAU,GAAG,UAAU,EAAE,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE;YACxB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,KAAK,EAAE,MAAM,CAAC,KAAK;YACnB,QAAQ,EAAE,MAAM,CAAC,QAAQ;YACzB,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK;SACnC,CAAC,CAAC;QACH,wEAAwE;QACxE,iDAAiD;QACjD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;YAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;YACvC,IAAI,MAAM,CAAC,IAAI;gBAAE,MAAM;YACvB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjC,CAAC;QACD,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,IAAI,CAAmB,UAAkB,EAAE,QAAgB;QACzD,IAAI,CAAC,YAAY,EAAE,CAAC;QACpB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACxC,IAAI,KAAK,EAAE,QAAQ,KAAK,QAAQ;YAAE,MAAM,aAAa,EAAE,CAAC;QACxD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QAC7B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;QACjC,OAAO;YACL,KAAK,EAAE,KAAK,CAAC,KAAqB;YAClC,QAAQ,EAAE,KAAK,CAAC,QAAa;SAC9B,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;CACF","sourcesContent":["import { randomUUID } from 'node:crypto';\n\nimport { ErrorCode, FsError } from './errors.js';\n\nconst DEFAULT_MAX_SNAPSHOTS = 32;\nconst DEFAULT_TTL_MS = 60 * 1000;\n\ninterface StoredPageSnapshot {\n readonly queryKey: string;\n readonly items: readonly unknown[];\n readonly metadata: unknown;\n readonly expiresAt: number;\n}\n\nexport interface PageSnapshot<T = unknown, M = unknown> {\n readonly items: readonly T[];\n readonly metadata: M;\n}\n\nexport interface PageSnapshotStoreOptions {\n readonly maxSnapshots?: number;\n readonly ttlMs?: number;\n readonly now?: () => number;\n}\n\n/**\n * The one cursor rejection: a cursor whose snapshot expired, was evicted, was\n * already consumed past its end, or belongs to a different query. Every case\n * has the same remedy, so they share one message.\n */\nexport function invalidCursor(): FsError {\n return new FsError(\n ErrorCode.INVALID_INPUT,\n 'Invalid cursor. Request the first page without a cursor.',\n );\n}\n\n/**\n * Short-lived snapshots of a completed query's full result set, so later pages\n * slice a stored array instead of re-scanning and re-sorting the filesystem.\n *\n * ponytail: bounded by snapshot count and TTL, not by bytes — 32 x 20,000\n * `list` entries or 32 x 10,000 `search_text` matches, each match retaining up\n * to 20 more line strings at `context: 10`, order of a few hundred MB held for\n * 60s, and the HTTP leg shares one store so any caller can drive it. If that\n * shows up as memory pressure, meter bytes the way `ResourceStore` does.\n */\nexport class PageSnapshotStore {\n private readonly byId = new Map<string, StoredPageSnapshot>();\n private readonly maxSnapshots: number;\n private readonly ttlMs: number;\n private readonly now: () => number;\n\n constructor(options: PageSnapshotStoreOptions = {}) {\n this.maxSnapshots = options.maxSnapshots ?? DEFAULT_MAX_SNAPSHOTS;\n this.ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;\n this.now = options.now ?? Date.now;\n }\n\n private pruneExpired(): void {\n const now = this.now();\n for (const [snapshotId, entry] of this.byId) {\n if (entry.expiresAt <= now) this.byId.delete(snapshotId);\n }\n }\n\n create(params: { queryKey: string; items: readonly unknown[]; metadata?: unknown }): string {\n this.pruneExpired();\n const snapshotId = randomUUID();\n this.byId.set(snapshotId, {\n queryKey: params.queryKey,\n items: params.items,\n metadata: params.metadata,\n expiresAt: this.now() + this.ttlMs,\n });\n // Oldest first: Map keeps insertion order and `read` re-inserts, so the\n // first key is the least recently used snapshot.\n while (this.byId.size > this.maxSnapshots) {\n const oldest = this.byId.keys().next();\n if (oldest.done) break;\n this.byId.delete(oldest.value);\n }\n return snapshotId;\n }\n\n read<T, M = undefined>(snapshotId: string, queryKey: string): PageSnapshot<T, M> {\n this.pruneExpired();\n const entry = this.byId.get(snapshotId);\n if (entry?.queryKey !== queryKey) throw invalidCursor();\n this.byId.delete(snapshotId);\n this.byId.set(snapshotId, entry);\n return {\n items: entry.items as readonly T[],\n metadata: entry.metadata as M,\n };\n }\n\n clear(): void {\n this.byId.clear();\n }\n}\n"]}
@@ -12,6 +12,13 @@ export interface DirentLike {
12
12
  isSymbolicLink(): boolean;
13
13
  }
14
14
  export declare function resolveEntryType(dirent: DirentLike): EntryType;
15
+ /**
16
+ * Warn once per (setting, value) that a configured value was rejected, and say
17
+ * what was used instead. console.error, not Logger: this module stays
18
+ * dependency-free to avoid import cycles, and a startup misconfiguration is
19
+ * worth printing even when the operator asked for errors only.
20
+ */
21
+ export declare function warnInvalidSetting(name: string, value: string, expected: string, fallback: string | number | boolean): void;
15
22
  /**
16
23
  * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.
17
24
  * Any other value warns once per var (when `name` is given) and reads as
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/core/primitives.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,WAAW,oDAAqD,CAAC;AAC9E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC;IAClB,cAAc,IAAI,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK9D;AAID;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAgBlF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM9D;AAID,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OAAqC,CAAC;AAE7E,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OACoB,CAAC;AAE5D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC"}
1
+ {"version":3,"file":"primitives.d.ts","sourceRoot":"","sources":["../../src/core/primitives.ts"],"names":[],"mappings":"AAEA;;;GAGG;AAEH,2EAA2E;AAC3E,eAAO,MAAM,WAAW,oDAAqD,CAAC;AAC9E,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC;AAErD,4FAA4F;AAC5F,MAAM,WAAW,UAAU;IACzB,WAAW,IAAI,OAAO,CAAC;IACvB,MAAM,IAAI,OAAO,CAAC;IAClB,cAAc,IAAI,OAAO,CAAC;CAC3B;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,SAAS,CAK9D;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAClC,IAAI,CAON;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAQlF;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,EAAE,CAM9D;AAID,eAAO,MAAM,UAAU,SAA+B,CAAC;AAEvD,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OAAqC,CAAC;AAE7E,eAAO,MAAM,OAAO,GAAI,MAAM,MAAM,KAAG,OACoB,CAAC;AAE5D,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,KAAG,MAAqC,CAAC"}
@@ -14,7 +14,20 @@ export function resolveEntryType(dirent) {
14
14
  return 'symlink';
15
15
  return 'other';
16
16
  }
17
- const warnedFlagValues = new Set();
17
+ const warnedSettings = new Set();
18
+ /**
19
+ * Warn once per (setting, value) that a configured value was rejected, and say
20
+ * what was used instead. console.error, not Logger: this module stays
21
+ * dependency-free to avoid import cycles, and a startup misconfiguration is
22
+ * worth printing even when the operator asked for errors only.
23
+ */
24
+ export function warnInvalidSetting(name, value, expected, fallback) {
25
+ const key = `${name}:${value}`;
26
+ if (warnedSettings.has(key))
27
+ return;
28
+ warnedSettings.add(key);
29
+ console.error(`[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`);
30
+ }
18
31
  /**
19
32
  * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.
20
33
  * Any other value warns once per var (when `name` is given) and reads as
@@ -27,13 +40,7 @@ export function parseTrueEnvFlag(value, name) {
27
40
  if (trimmed === 'true' || trimmed === '1')
28
41
  return true;
29
42
  if (name && trimmed !== '' && trimmed !== 'false' && trimmed !== '0') {
30
- const key = `${name}:${trimmed}`;
31
- if (!warnedFlagValues.has(key)) {
32
- warnedFlagValues.add(key);
33
- // console.error, not Logger: this module stays dependency-free to avoid
34
- // import cycles (same precedent as parseLogLevel in observability.ts).
35
- console.error(`[warning] Invalid ${name} value: ${value} (must be "true" or "1"). Using default: false`);
36
- }
43
+ warnInvalidSetting(name, value, '"true" or "1"', false);
37
44
  }
38
45
  return false;
39
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"primitives.js","sourceRoot":"","sources":["../../src/core/primitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;;GAGG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,CAAU,CAAC;AAU9E,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IACjD,IAAI,MAAM,CAAC,WAAW,EAAE;QAAE,OAAO,WAAW,CAAC;IAC7C,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,MAAM,CAAC;IACnC,IAAI,MAAM,CAAC,cAAc,EAAE;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,gBAAgB,GAAG,IAAI,GAAG,EAAU,CAAC;AAE3C;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAyB,EAAE,IAAa;IACvE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,IAAI,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;QACrE,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;QACjC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/B,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAC1B,wEAAwE;YACxE,uEAAuE;YACvE,OAAO,CAAC,KAAK,CACX,qBAAqB,IAAI,WAAW,KAAK,gDAAgD,CAC1F,CAAC;QACJ,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,OAAO,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG;SACP,KAAK,CAAC,SAAS,CAAC;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,gFAAgF;AAEhF,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAEvD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC;AAE7E,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAC/C,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC","sourcesContent":["import { delimiter } from 'node:path';\n\n/**\n * Minimal shared primitives with no intra-package dependencies.\n * Kept separate to avoid import cycles between observability.ts and util.ts.\n */\n\n/** The four filesystem entry types; published as the `FileType` schema. */\nexport const ENTRY_TYPES = ['file', 'directory', 'symlink', 'other'] as const;\nexport type EntryType = (typeof ENTRY_TYPES)[number];\n\n/** The three predicates both `Dirent` and `Stats` expose - all `resolveEntryType` needs. */\nexport interface DirentLike {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n}\n\nexport function resolveEntryType(dirent: DirentLike): EntryType {\n if (dirent.isDirectory()) return 'directory';\n if (dirent.isFile()) return 'file';\n if (dirent.isSymbolicLink()) return 'symlink';\n return 'other';\n}\n\nconst warnedFlagValues = new Set<string>();\n\n/**\n * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.\n * Any other value warns once per var (when `name` is given) and reads as\n * false, so a typo like `FS_ALLOW_SENSITIVE=yes` is never silent.\n */\nexport function parseTrueEnvFlag(value: string | undefined, name?: string): boolean {\n if (value === undefined) return false;\n const trimmed = value.trim().toLowerCase();\n if (trimmed === 'true' || trimmed === '1') return true;\n if (name && trimmed !== '' && trimmed !== 'false' && trimmed !== '0') {\n const key = `${name}:${trimmed}`;\n if (!warnedFlagValues.has(key)) {\n warnedFlagValues.add(key);\n // console.error, not Logger: this module stays dependency-free to avoid\n // import cycles (same precedent as parseLogLevel in observability.ts).\n console.error(\n `[warning] Invalid ${name} value: ${value} (must be \"true\" or \"1\"). Using default: false`,\n );\n }\n }\n return false;\n}\n\nexport function escapeRegexLiteral(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport function splitDirList(val: string | undefined): string[] {\n if (!val) return [];\n return val\n .split(delimiter)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n// ─── Path primitives (moved from path.ts to break the path↔sensitive cycle) ──\n\nexport const IS_WINDOWS = process.platform === 'win32';\n\nexport const isSlash = (code: number): boolean => code === 47 || code === 92;\n\nexport const isAlpha = (code: number): boolean =>\n (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n\nexport const toPosixPath = (value: string): string => value.replaceAll('\\\\', '/');\n"]}
1
+ {"version":3,"file":"primitives.js","sourceRoot":"","sources":["../../src/core/primitives.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;;GAGG;AAEH,2EAA2E;AAC3E,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,CAAU,CAAC;AAU9E,MAAM,UAAU,gBAAgB,CAAC,MAAkB;IACjD,IAAI,MAAM,CAAC,WAAW,EAAE;QAAE,OAAO,WAAW,CAAC;IAC7C,IAAI,MAAM,CAAC,MAAM,EAAE;QAAE,OAAO,MAAM,CAAC;IACnC,IAAI,MAAM,CAAC,cAAc,EAAE;QAAE,OAAO,SAAS,CAAC;IAC9C,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,cAAc,GAAG,IAAI,GAAG,EAAU,CAAC;AAEzC;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAY,EACZ,KAAa,EACb,QAAgB,EAChB,QAAmC;IAEnC,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC;IAC/B,IAAI,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO;IACpC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACxB,OAAO,CAAC,KAAK,CACX,qBAAqB,IAAI,WAAW,KAAK,aAAa,QAAQ,qBAAqB,MAAM,CAAC,QAAQ,CAAC,EAAE,CACtG,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAyB,EAAE,IAAa;IACvE,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,OAAO,KAAK,MAAM,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IACvD,IAAI,IAAI,IAAI,OAAO,KAAK,EAAE,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,GAAG,EAAE,CAAC;QACrE,kBAAkB,CAAC,IAAI,EAAE,KAAK,EAAE,eAAe,EAAE,KAAK,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,GAAW;IAC5C,OAAO,GAAG,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG;SACP,KAAK,CAAC,SAAS,CAAC;SAChB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACjC,CAAC;AAED,gFAAgF;AAEhF,MAAM,CAAC,MAAM,UAAU,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC;AAEvD,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAAC,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,EAAE,CAAC;AAE7E,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,IAAY,EAAW,EAAE,CAC/C,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,IAAI,IAAI,GAAG,CAAC,CAAC;AAE5D,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC","sourcesContent":["import { delimiter } from 'node:path';\n\n/**\n * Minimal shared primitives with no intra-package dependencies.\n * Kept separate to avoid import cycles between observability.ts and util.ts.\n */\n\n/** The four filesystem entry types; published as the `FileType` schema. */\nexport const ENTRY_TYPES = ['file', 'directory', 'symlink', 'other'] as const;\nexport type EntryType = (typeof ENTRY_TYPES)[number];\n\n/** The three predicates both `Dirent` and `Stats` expose - all `resolveEntryType` needs. */\nexport interface DirentLike {\n isDirectory(): boolean;\n isFile(): boolean;\n isSymbolicLink(): boolean;\n}\n\nexport function resolveEntryType(dirent: DirentLike): EntryType {\n if (dirent.isDirectory()) return 'directory';\n if (dirent.isFile()) return 'file';\n if (dirent.isSymbolicLink()) return 'symlink';\n return 'other';\n}\n\nconst warnedSettings = new Set<string>();\n\n/**\n * Warn once per (setting, value) that a configured value was rejected, and say\n * what was used instead. console.error, not Logger: this module stays\n * dependency-free to avoid import cycles, and a startup misconfiguration is\n * worth printing even when the operator asked for errors only.\n */\nexport function warnInvalidSetting(\n name: string,\n value: string,\n expected: string,\n fallback: string | number | boolean,\n): void {\n const key = `${name}:${value}`;\n if (warnedSettings.has(key)) return;\n warnedSettings.add(key);\n console.error(\n `[warning] Invalid ${name} value: ${value} (must be ${expected}). Using default: ${String(fallback)}`,\n );\n}\n\n/**\n * Parse a boolean env flag: `true`/`1` enable, `false`/`0`/empty disable.\n * Any other value warns once per var (when `name` is given) and reads as\n * false, so a typo like `FS_ALLOW_SENSITIVE=yes` is never silent.\n */\nexport function parseTrueEnvFlag(value: string | undefined, name?: string): boolean {\n if (value === undefined) return false;\n const trimmed = value.trim().toLowerCase();\n if (trimmed === 'true' || trimmed === '1') return true;\n if (name && trimmed !== '' && trimmed !== 'false' && trimmed !== '0') {\n warnInvalidSetting(name, value, '\"true\" or \"1\"', false);\n }\n return false;\n}\n\nexport function escapeRegexLiteral(str: string): string {\n return str.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n\nexport function splitDirList(val: string | undefined): string[] {\n if (!val) return [];\n return val\n .split(delimiter)\n .map((p) => p.trim())\n .filter((p) => p.length > 0);\n}\n\n// ─── Path primitives (moved from path.ts to break the path↔sensitive cycle) ──\n\nexport const IS_WINDOWS = process.platform === 'win32';\n\nexport const isSlash = (code: number): boolean => code === 47 || code === 92;\n\nexport const isAlpha = (code: number): boolean =>\n (code >= 65 && code <= 90) || (code >= 97 && code <= 122);\n\nexport const toPosixPath = (value: string): string => value.replaceAll('\\\\', '/');\n"]}
@@ -7,6 +7,9 @@ interface SearchResult {
7
7
  column: number;
8
8
  content: string;
9
9
  matchCount?: number;
10
+ /** Up to `context` lines either side of the match; absent when context is 0. */
11
+ before?: string[];
12
+ after?: string[];
10
13
  }
11
14
  export type Regex = RE2;
12
15
  export interface RegexCompileOptions {
@@ -77,10 +80,11 @@ export interface SearchContentOptions {
77
80
  isRegex?: boolean;
78
81
  maxResults?: number;
79
82
  filePattern?: string;
80
- excludePatterns?: string[];
81
- respectGitignore?: boolean;
83
+ skipIgnored?: boolean;
82
84
  includeHidden?: boolean;
83
85
  maxDepth?: number;
86
+ /** Lines of context to carry either side of each match; 0 (default) carries none. */
87
+ context?: number;
84
88
  signal?: AbortSignal;
85
89
  }
86
90
  export interface SearchContentOutcome {
@@ -109,11 +113,11 @@ export interface SearchContentOutcome {
109
113
  };
110
114
  }
111
115
  export declare function searchContent(directory: string, pattern: string, options: SearchContentOptions, pathGuard: PathGuard): Promise<SearchContentOutcome>;
112
- export declare function searchFiles(directory: string, pattern: string, excludePatterns: string[], options: {
116
+ export declare function searchFiles(directory: string, pattern: string, options: {
113
117
  maxResults?: number;
114
118
  includeHidden?: boolean;
115
119
  sortBy?: 'name' | 'path';
116
- respectGitignore?: boolean;
120
+ skipIgnored?: boolean;
117
121
  maxDepth?: number;
118
122
  signal?: AbortSignal;
119
123
  }, pathGuard: PathGuard): Promise<{
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/core/search.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAGxC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAI3C,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,MAAM,KAAK,GAAG,GAAG,CAAC;AACxB,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAKD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB,GAAG,KAAK,CAWtF;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,GAAG,IAAI,CAOxD;AAED,MAAM,WAAW,UAAU;IACzB,wBAAwB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,gEAAgE;IAChE,MAAM,EAAE,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;IAC/B,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAC1C,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,uCAAuC;IACvC,GAAG,EAAE,MAAM,CAAC;CACb;AAcD;;;;;;;;;;;;;;;GAeG;AACH,wBAAiB,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC,UAAU,EAAE,SAAS,CAAC,CA2BzF;AAoBD,MAAM,WAAW,oBAAoB;IACnC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qFAAqF;IACrF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,OAAO,EAAE;QACP;;;;WAIG;QACH,aAAa,EAAE,MAAM,CAAC;QACtB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,OAAO,CAAC;QACnB,4DAA4D;QAC5D,mBAAmB,EAAE,MAAM,CAAC;QAC5B,4DAA4D;QAC5D,eAAe,EAAE,MAAM,CAAC;QACxB;;;;WAIG;QACH,aAAa,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;KAC1C,CAAC;CACH;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,oBAAoB,EAC7B,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,oBAAoB,CAAC,CA+G/B;AA0BD,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,EACD,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC;IACT,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IAC5B,OAAO,EAAE;QACP,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,OAAO,CAAC;QACnB,mBAAmB,EAAE,MAAM,CAAC;QAC5B,4EAA4E;QAC5E,aAAa,CAAC,EAAE,YAAY,GAAG,SAAS,CAAC;KAC1C,CAAC;CACH,CAAC,CA4CD"}