craftdriver 1.6.0 → 1.8.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 (229) hide show
  1. package/CHANGELOG.md +35 -6
  2. package/README.md +17 -6
  3. package/dist/cli/agentSession.d.ts +62 -0
  4. package/dist/cli/agentSession.d.ts.map +1 -0
  5. package/dist/cli/agentSession.js +100 -0
  6. package/dist/cli/agentSession.js.map +1 -0
  7. package/dist/cli/artifactPaths.d.ts +42 -0
  8. package/dist/cli/artifactPaths.d.ts.map +1 -0
  9. package/dist/cli/artifactPaths.js +144 -0
  10. package/dist/cli/artifactPaths.js.map +1 -0
  11. package/dist/cli/bounds.d.ts +88 -0
  12. package/dist/cli/bounds.d.ts.map +1 -0
  13. package/dist/cli/bounds.js +128 -0
  14. package/dist/cli/bounds.js.map +1 -0
  15. package/dist/cli/client.d.ts +3 -0
  16. package/dist/cli/client.d.ts.map +1 -1
  17. package/dist/cli/client.js +24 -8
  18. package/dist/cli/client.js.map +1 -1
  19. package/dist/cli/daemon.d.ts +3 -0
  20. package/dist/cli/daemon.d.ts.map +1 -1
  21. package/dist/cli/daemon.js +152 -39
  22. package/dist/cli/daemon.js.map +1 -1
  23. package/dist/cli/defaults.d.ts +23 -3
  24. package/dist/cli/defaults.d.ts.map +1 -1
  25. package/dist/cli/defaults.js +50 -5
  26. package/dist/cli/defaults.js.map +1 -1
  27. package/dist/cli/dispatcher.d.ts +54 -11
  28. package/dist/cli/dispatcher.d.ts.map +1 -1
  29. package/dist/cli/dispatcher.js +851 -54
  30. package/dist/cli/dispatcher.js.map +1 -1
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +305 -56
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/cli/init.d.ts +14 -14
  35. package/dist/cli/init.d.ts.map +1 -1
  36. package/dist/cli/init.js +328 -180
  37. package/dist/cli/init.js.map +1 -1
  38. package/dist/cli/journal.d.ts +153 -0
  39. package/dist/cli/journal.d.ts.map +1 -0
  40. package/dist/cli/journal.js +252 -0
  41. package/dist/cli/journal.js.map +1 -0
  42. package/dist/cli/lineReader.d.ts +33 -0
  43. package/dist/cli/lineReader.d.ts.map +1 -0
  44. package/dist/cli/lineReader.js +66 -0
  45. package/dist/cli/lineReader.js.map +1 -0
  46. package/dist/cli/locatorCandidates.d.ts +44 -0
  47. package/dist/cli/locatorCandidates.d.ts.map +1 -0
  48. package/dist/cli/locatorCandidates.js +190 -0
  49. package/dist/cli/locatorCandidates.js.map +1 -0
  50. package/dist/cli/mcp/artifacts.d.ts +39 -1
  51. package/dist/cli/mcp/artifacts.d.ts.map +1 -1
  52. package/dist/cli/mcp/artifacts.js +104 -9
  53. package/dist/cli/mcp/artifacts.js.map +1 -1
  54. package/dist/cli/mcp/bounds.d.ts +65 -0
  55. package/dist/cli/mcp/bounds.d.ts.map +1 -0
  56. package/dist/cli/mcp/bounds.js +124 -0
  57. package/dist/cli/mcp/bounds.js.map +1 -0
  58. package/dist/cli/mcp/params.d.ts +38 -0
  59. package/dist/cli/mcp/params.d.ts.map +1 -0
  60. package/dist/cli/mcp/params.js +174 -0
  61. package/dist/cli/mcp/params.js.map +1 -0
  62. package/dist/cli/mcp/server.d.ts +94 -16
  63. package/dist/cli/mcp/server.d.ts.map +1 -1
  64. package/dist/cli/mcp/server.js +304 -150
  65. package/dist/cli/mcp/server.js.map +1 -1
  66. package/dist/cli/mcp/tools.d.ts +62 -17
  67. package/dist/cli/mcp/tools.d.ts.map +1 -1
  68. package/dist/cli/mcp/tools.js +373 -158
  69. package/dist/cli/mcp/tools.js.map +1 -1
  70. package/dist/cli/pageSemantics.d.ts +42 -0
  71. package/dist/cli/pageSemantics.d.ts.map +1 -0
  72. package/dist/cli/pageSemantics.js +194 -0
  73. package/dist/cli/pageSemantics.js.map +1 -0
  74. package/dist/cli/parseArgs.d.ts +6 -18
  75. package/dist/cli/parseArgs.d.ts.map +1 -1
  76. package/dist/cli/parseArgs.js +689 -45
  77. package/dist/cli/parseArgs.js.map +1 -1
  78. package/dist/cli/protocol.d.ts +8 -0
  79. package/dist/cli/protocol.d.ts.map +1 -1
  80. package/dist/cli/sessionRegistry.d.ts +42 -0
  81. package/dist/cli/sessionRegistry.d.ts.map +1 -0
  82. package/dist/cli/sessionRegistry.js +99 -0
  83. package/dist/cli/sessionRegistry.js.map +1 -0
  84. package/dist/cli/snapshot.d.ts +100 -18
  85. package/dist/cli/snapshot.d.ts.map +1 -1
  86. package/dist/cli/snapshot.js +248 -102
  87. package/dist/cli/snapshot.js.map +1 -1
  88. package/dist/cli/stateStore.d.ts +50 -0
  89. package/dist/cli/stateStore.d.ts.map +1 -0
  90. package/dist/cli/stateStore.js +122 -0
  91. package/dist/cli/stateStore.js.map +1 -0
  92. package/dist/index.d.ts +2 -1
  93. package/dist/index.d.ts.map +1 -1
  94. package/dist/index.js +2 -0
  95. package/dist/index.js.map +1 -1
  96. package/dist/lib/bidi/index.d.ts +1 -1
  97. package/dist/lib/bidi/index.d.ts.map +1 -1
  98. package/dist/lib/bidi/index.js +2 -2
  99. package/dist/lib/bidi/index.js.map +1 -1
  100. package/dist/lib/bidi/logs.d.ts +3 -0
  101. package/dist/lib/bidi/logs.d.ts.map +1 -1
  102. package/dist/lib/bidi/logs.js +7 -0
  103. package/dist/lib/bidi/logs.js.map +1 -1
  104. package/dist/lib/bidi/network.d.ts +14 -1
  105. package/dist/lib/bidi/network.d.ts.map +1 -1
  106. package/dist/lib/bidi/network.js +45 -7
  107. package/dist/lib/bidi/network.js.map +1 -1
  108. package/dist/lib/bidi/storage.d.ts +30 -6
  109. package/dist/lib/bidi/storage.d.ts.map +1 -1
  110. package/dist/lib/bidi/storage.js +190 -45
  111. package/dist/lib/bidi/storage.js.map +1 -1
  112. package/dist/lib/bidi/types.d.ts +7 -2
  113. package/dist/lib/bidi/types.d.ts.map +1 -1
  114. package/dist/lib/browser.d.ts +38 -3
  115. package/dist/lib/browser.d.ts.map +1 -1
  116. package/dist/lib/browser.js +196 -30
  117. package/dist/lib/browser.js.map +1 -1
  118. package/dist/lib/browserContext.d.ts +54 -18
  119. package/dist/lib/browserContext.d.ts.map +1 -1
  120. package/dist/lib/browserContext.js +295 -92
  121. package/dist/lib/browserContext.js.map +1 -1
  122. package/dist/lib/by.d.ts +3 -3
  123. package/dist/lib/by.d.ts.map +1 -1
  124. package/dist/lib/by.js +7 -3
  125. package/dist/lib/by.js.map +1 -1
  126. package/dist/lib/driver.d.ts +7 -0
  127. package/dist/lib/driver.d.ts.map +1 -1
  128. package/dist/lib/driver.js +12 -0
  129. package/dist/lib/driver.js.map +1 -1
  130. package/dist/lib/errors.d.ts +15 -0
  131. package/dist/lib/errors.d.ts.map +1 -1
  132. package/dist/lib/errors.js +15 -0
  133. package/dist/lib/errors.js.map +1 -1
  134. package/dist/lib/initScript.d.ts +9 -0
  135. package/dist/lib/initScript.d.ts.map +1 -0
  136. package/dist/lib/initScript.js +14 -0
  137. package/dist/lib/initScript.js.map +1 -0
  138. package/dist/lib/page.d.ts +22 -0
  139. package/dist/lib/page.d.ts.map +1 -1
  140. package/dist/lib/page.js +48 -0
  141. package/dist/lib/page.js.map +1 -1
  142. package/dist/lib/secureFile.d.ts +8 -0
  143. package/dist/lib/secureFile.d.ts.map +1 -0
  144. package/dist/lib/secureFile.js +67 -0
  145. package/dist/lib/secureFile.js.map +1 -0
  146. package/dist/lib/sessionStateValidation.d.ts +35 -0
  147. package/dist/lib/sessionStateValidation.d.ts.map +1 -0
  148. package/dist/lib/sessionStateValidation.js +223 -0
  149. package/dist/lib/sessionStateValidation.js.map +1 -0
  150. package/dist/lib/timing.d.ts +8 -0
  151. package/dist/lib/timing.d.ts.map +1 -1
  152. package/dist/lib/timing.js +8 -0
  153. package/dist/lib/timing.js.map +1 -1
  154. package/dist/lib/tracing.d.ts +2 -1
  155. package/dist/lib/tracing.d.ts.map +1 -1
  156. package/dist/lib/tracing.js +12 -1
  157. package/dist/lib/tracing.js.map +1 -1
  158. package/dist/lib/visual/antialias.d.ts +41 -0
  159. package/dist/lib/visual/antialias.d.ts.map +1 -0
  160. package/dist/lib/visual/antialias.js +126 -0
  161. package/dist/lib/visual/antialias.js.map +1 -0
  162. package/dist/lib/visual/codec.d.ts +24 -0
  163. package/dist/lib/visual/codec.d.ts.map +1 -0
  164. package/dist/lib/visual/codec.js +11 -0
  165. package/dist/lib/visual/codec.js.map +1 -0
  166. package/dist/lib/visual/compare.d.ts +38 -0
  167. package/dist/lib/visual/compare.d.ts.map +1 -0
  168. package/dist/lib/visual/compare.js +209 -0
  169. package/dist/lib/visual/compare.js.map +1 -0
  170. package/dist/lib/visual/diff.d.ts +18 -0
  171. package/dist/lib/visual/diff.d.ts.map +1 -0
  172. package/dist/lib/visual/diff.js +23 -0
  173. package/dist/lib/visual/diff.js.map +1 -0
  174. package/dist/lib/visual/expectScreenshot.d.ts +65 -0
  175. package/dist/lib/visual/expectScreenshot.d.ts.map +1 -0
  176. package/dist/lib/visual/expectScreenshot.js +287 -0
  177. package/dist/lib/visual/expectScreenshot.js.map +1 -0
  178. package/dist/lib/visual/index.d.ts +10 -0
  179. package/dist/lib/visual/index.d.ts.map +1 -0
  180. package/dist/lib/visual/index.js +9 -0
  181. package/dist/lib/visual/index.js.map +1 -0
  182. package/dist/lib/visual/pixelMath.d.ts +32 -0
  183. package/dist/lib/visual/pixelMath.d.ts.map +1 -0
  184. package/dist/lib/visual/pixelMath.js +70 -0
  185. package/dist/lib/visual/pixelMath.js.map +1 -0
  186. package/dist/lib/visual/pngHeader.d.ts +27 -0
  187. package/dist/lib/visual/pngHeader.d.ts.map +1 -0
  188. package/dist/lib/visual/pngHeader.js +63 -0
  189. package/dist/lib/visual/pngHeader.js.map +1 -0
  190. package/dist/lib/visual/pngjsCodec.d.ts +22 -0
  191. package/dist/lib/visual/pngjsCodec.d.ts.map +1 -0
  192. package/dist/lib/visual/pngjsCodec.js +70 -0
  193. package/dist/lib/visual/pngjsCodec.js.map +1 -0
  194. package/dist/lib/visual/types.d.ts +89 -0
  195. package/dist/lib/visual/types.d.ts.map +1 -0
  196. package/dist/lib/visual/types.js +20 -0
  197. package/dist/lib/visual/types.js.map +1 -0
  198. package/dist/lib/visual/visualError.d.ts +32 -0
  199. package/dist/lib/visual/visualError.d.ts.map +1 -0
  200. package/dist/lib/visual/visualError.js +59 -0
  201. package/dist/lib/visual/visualError.js.map +1 -0
  202. package/dist/lib/wait.d.ts.map +1 -1
  203. package/dist/lib/wait.js +7 -1
  204. package/dist/lib/wait.js.map +1 -1
  205. package/docs/agents.md +56 -61
  206. package/docs/api-reference.md +16 -1
  207. package/docs/browser-api.md +24 -4
  208. package/docs/browser-context.md +18 -12
  209. package/docs/cli.md +346 -26
  210. package/docs/error-codes.md +6 -4
  211. package/docs/getting-started.md +6 -1
  212. package/docs/index.md +1 -1
  213. package/docs/mcp.md +129 -62
  214. package/docs/public/examples/agent-actions.html +48 -0
  215. package/docs/public/examples/agent-debug.html +86 -0
  216. package/docs/public/examples/agent-late-mutation.html +18 -0
  217. package/docs/public/examples/agent-ref-shift.html +33 -0
  218. package/docs/recipes/login-once-reuse-session.md +32 -4
  219. package/docs/screenshots.md +15 -6
  220. package/docs/session-management.md +64 -3
  221. package/docs/visual-testing.md +254 -0
  222. package/docs/why-craftdriver.md +4 -3
  223. package/package.json +5 -1
  224. package/skills/craftdriver/SKILL.md +59 -75
  225. package/skills/craftdriver/agents/openai.yaml +4 -0
  226. package/skills/craftdriver/cheatsheet.md +18 -4
  227. package/skills/craftdriver/cli.md +194 -68
  228. package/skills/craftdriver/patterns.md +14 -8
  229. package/skills/craftdriver/workflow.md +149 -0
@@ -0,0 +1,190 @@
1
+ import { CraftdriverError, ErrorCode } from '../lib/errors.js';
2
+ import { parseSelector, describeSelector } from './selector.js';
3
+ import { PAGE_SEMANTICS_JS } from './pageSemantics.js';
4
+ /** Ordered best-first; the cap keeps output bounded for agent contexts. */
5
+ const MAX_CANDIDATES = 8;
6
+ const MAX_NAME = 80;
7
+ /**
8
+ * Ids and names that are obviously build- or framework-generated. A CSS
9
+ * selector built on one of these looks durable and is not, which is worse
10
+ * than offering no CSS candidate at all.
11
+ */
12
+ function looksGenerated(value) {
13
+ return (/^[0-9]/.test(value) ||
14
+ /[0-9a-f]{8,}/i.test(value) ||
15
+ /^(:r|ember\d|mui-|radix-|headlessui-|react-aria-)/.test(value) ||
16
+ /\d{4,}/.test(value));
17
+ }
18
+ /**
19
+ * Whether a page-derived string can be used verbatim in an exact-match
20
+ * locator. Long values are rejected rather than truncated: `By.role` and
21
+ * friends match exactly, so an elided value resolves to nothing while
22
+ * looking like a usable candidate.
23
+ */
24
+ function usableAsExactMatch(value) {
25
+ return value.length > 0 && value.length <= MAX_NAME;
26
+ }
27
+ export async function locatorCandidates(browser, by, limit = 5) {
28
+ const els = await browser.findAll(by);
29
+ if (els.length === 0) {
30
+ throw new CraftdriverError(ErrorCode.NO_MATCH, `locators: no element matches ${describeSelector(by)}`, { detail: { selector: describeSelector(by) } });
31
+ }
32
+ // Mark the target, then read its evidence from page scope. The mark is
33
+ // what lets validation prove a candidate resolved back to *this* node.
34
+ const nonce = `p${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
35
+ await els[0].evaluate((node, stamp) => {
36
+ node
37
+ .setAttribute('data-craftdriver-probe', stamp);
38
+ }, nonce);
39
+ try {
40
+ const page = await browser.activePage();
41
+ const raw = await page.evaluate(jsEvidence(nonce));
42
+ if (!raw || typeof raw !== 'object') {
43
+ return { target: describeSelector(by), candidates: [], best: null, note: 'element not readable' };
44
+ }
45
+ const evidence = raw;
46
+ const proposed = propose(evidence);
47
+ const validated = [];
48
+ for (const candidate of proposed.slice(0, MAX_CANDIDATES)) {
49
+ validated.push(await validate(browser, candidate, nonce));
50
+ if (validated.filter((c) => c.status === 'unique').length >= limit)
51
+ break;
52
+ }
53
+ const best = validated.find((c) => c.status === 'unique') ?? null;
54
+ return {
55
+ target: describeSelector(by),
56
+ candidates: validated.slice(0, Math.max(limit, 1) + 2),
57
+ best: best ? best.selector : null,
58
+ ...(best
59
+ ? {}
60
+ : {
61
+ note: 'no durable locator resolved uniquely; add a data-testid to this element ' +
62
+ 'rather than committing a positional selector',
63
+ }),
64
+ };
65
+ }
66
+ finally {
67
+ // Never leave the probe attribute on the page — it would show up in
68
+ // the agent's next snapshot and in any DOM the user inspects.
69
+ await els[0]
70
+ .evaluate((node) => {
71
+ node
72
+ .removeAttribute('data-craftdriver-probe');
73
+ })
74
+ .catch(() => undefined);
75
+ }
76
+ }
77
+ /**
78
+ * Page-side evidence extraction. Runs as a string body (the build has no
79
+ * DOM lib) and reads only what the live document actually offers.
80
+ * `nonce` is generated here and JSON-encoded, so it cannot break out.
81
+ *
82
+ * Role and name come from the shared extractor, so a candidate can never
83
+ * describe the element differently from the snapshot the agent just read.
84
+ * `role` is deliberately the ARIA role or nothing: proposing `role=` for an
85
+ * element that has no ARIA role produces a locator that resolves to zero
86
+ * elements, which is worse than offering no role candidate at all.
87
+ */
88
+ function jsEvidence(nonce) {
89
+ return `
90
+ ${PAGE_SEMANTICS_JS}
91
+ const el = document.querySelector('[data-craftdriver-probe=' + ${JSON.stringify(JSON.stringify(nonce))} + ']');
92
+ if (!el) return null;
93
+ const tag = el.tagName.toLowerCase();
94
+ const name = accName(el);
95
+ const text = (el.textContent || '').trim().replace(/\\s+/g, ' ');
96
+ return {
97
+ role: ariaRole(el),
98
+ name: name || null,
99
+ label: associatedLabel(el),
100
+ testid: el.getAttribute('data-testid') || el.getAttribute('data-test-id'),
101
+ text: text || null,
102
+ id: el.id || null,
103
+ nameAttr: el.getAttribute('name'),
104
+ tag: tag,
105
+ nonce: ${JSON.stringify(nonce)}
106
+ };
107
+ `;
108
+ }
109
+ /** Build candidates in durability order. */
110
+ function propose(e) {
111
+ const out = [];
112
+ // 1. Role + accessible name — survives restyling and DOM reshuffles.
113
+ //
114
+ // Never truncate a value destined for an exact-match locator: an
115
+ // ellipsis guarantees it resolves to nothing. Skip instead, and let a
116
+ // later candidate carry the element. `]` would also break the
117
+ // `role=x[name=y]` grammar, which has no escape for it.
118
+ if (e.role && e.name && usableAsExactMatch(e.name) && !e.name.includes(']')) {
119
+ out.push({
120
+ kind: 'role',
121
+ selector: `role=${e.role}[name=${e.name}]`,
122
+ code: `By.role(${JSON.stringify(e.role)}, { name: ${JSON.stringify(e.name)} })`,
123
+ });
124
+ }
125
+ // 2. Associated label — the thing a user actually reads for form fields.
126
+ if (e.label && usableAsExactMatch(e.label)) {
127
+ out.push({
128
+ kind: 'label',
129
+ selector: `label=${e.label}`,
130
+ code: `By.labelText(${JSON.stringify(e.label)})`,
131
+ });
132
+ }
133
+ // 3. Test id — durable by contract when the app ships one.
134
+ if (e.testid) {
135
+ out.push({
136
+ kind: 'testid',
137
+ selector: `testid=${e.testid}`,
138
+ code: `By.testId(${JSON.stringify(e.testid)})`,
139
+ });
140
+ }
141
+ // 4. Text — durable only where it is unique, which validation decides.
142
+ if (e.text && usableAsExactMatch(e.text)) {
143
+ out.push({
144
+ kind: 'text',
145
+ selector: `text=${e.text}`,
146
+ code: `By.text(${JSON.stringify(e.text)})`,
147
+ });
148
+ }
149
+ // 5. Minimal CSS, last, and only when it is not obviously generated.
150
+ //
151
+ // The id also has to survive being written bare into a selector: `.`,
152
+ // `:` and friends are CSS syntax, so `#user.email` silently means
153
+ // "id user AND class email", and a quote breaks the generated code.
154
+ if (e.id && !looksGenerated(e.id) && /^[A-Za-z][A-Za-z0-9_-]*$/.test(e.id)) {
155
+ out.push({
156
+ kind: 'css',
157
+ selector: `#${e.id}`,
158
+ code: `By.css(${JSON.stringify('#' + e.id)})`,
159
+ });
160
+ }
161
+ if (e.nameAttr && !looksGenerated(e.nameAttr)) {
162
+ const css = `${e.tag}[name=${JSON.stringify(e.nameAttr)}]`;
163
+ out.push({ kind: 'css', selector: `css=${css}`, code: `By.css(${JSON.stringify(css)})` });
164
+ }
165
+ return out;
166
+ }
167
+ /**
168
+ * Re-resolve a candidate against the live document.
169
+ *
170
+ * Uniqueness alone is not enough — a selector can match exactly one
171
+ * element that is not the one the agent asked about. The probe attribute
172
+ * settles identity.
173
+ */
174
+ async function validate(browser, candidate, nonce) {
175
+ try {
176
+ const els = await browser.findAll(parseSelector(candidate.selector));
177
+ if (els.length === 0)
178
+ return { ...candidate, status: 'missing', matches: 0 };
179
+ if (els.length > 1)
180
+ return { ...candidate, status: 'ambiguous', matches: els.length };
181
+ const hit = await els[0].getAttribute('data-craftdriver-probe').catch(() => null);
182
+ return hit === nonce
183
+ ? { ...candidate, status: 'unique', matches: 1 }
184
+ : { ...candidate, status: 'ambiguous', matches: 1 };
185
+ }
186
+ catch {
187
+ return { ...candidate, status: 'missing', matches: 0 };
188
+ }
189
+ }
190
+ //# sourceMappingURL=locatorCandidates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locatorCandidates.js","sourceRoot":"","sources":["../../src/cli/locatorCandidates.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,39 @@
1
+ import { CraftdriverError } from '../../lib/errors.js';
1
2
  export interface ArtifactWriteResult {
2
3
  path: string;
3
4
  bytes: number;
4
5
  }
6
+ /**
7
+ * Quota for one session's spill directory.
8
+ *
9
+ * The store is written to automatically, on every oversized result, for the
10
+ * whole life of a long-running session — and nothing deletes it. Without a
11
+ * quota a loop that snapshots a large page fills the disk silently, which is
12
+ * a worse failure than a refused spill: the caller falls back to an inline
13
+ * preview and the session keeps working.
14
+ */
15
+ export declare const MAX_ARTIFACTS = 500;
16
+ export declare const MAX_TOTAL_BYTES: number;
17
+ /** Raised when a write would exceed the session's artifact quota. */
18
+ export declare class ArtifactQuotaError extends CraftdriverError {
19
+ constructor(message: string);
20
+ }
21
+ export interface ArtifactStoreLimits {
22
+ /** Test/embedding override; production uses {@link MAX_ARTIFACTS}. */
23
+ maxArtifacts?: number;
24
+ /** Test/embedding override; production uses {@link MAX_TOTAL_BYTES}. */
25
+ maxTotalBytes?: number;
26
+ }
5
27
  export declare class ArtifactStore {
6
28
  private dir;
7
29
  private ready;
8
30
  private counter;
9
- constructor(rootOverride?: string);
31
+ private artifactCount;
32
+ private totalBytes;
33
+ private reservations;
34
+ private readonly maxArtifacts;
35
+ private readonly maxTotalBytes;
36
+ constructor(rootOverride?: string, limits?: ArtifactStoreLimits);
10
37
  /** Absolute path to the artifact directory (may not exist yet). */
11
38
  get directory(): string;
12
39
  private ensure;
@@ -16,12 +43,23 @@ export declare class ArtifactStore {
16
43
  * `screenshot.png` or `eval.json`.
17
44
  */
18
45
  write(nameHint: string, data: Buffer | string): Promise<ArtifactWriteResult>;
46
+ private assertWithinQuota;
19
47
  /**
20
48
  * Allocate a path for an artifact the caller will write itself (e.g.
21
49
  * pass to `browser.screenshot({ path })`). Doesn't touch the disk
22
50
  * beyond `mkdir -p` of the directory.
23
51
  */
24
52
  allocate(nameHint: string): Promise<string>;
53
+ /**
54
+ * Account for a file written to a path returned by {@link allocate}.
55
+ *
56
+ * Screenshots are written by the browser, so their size is known only after
57
+ * capture. If the actual file would cross the quota, remove that one file and
58
+ * fail the tool call rather than letting the supposedly bounded store grow.
59
+ */
60
+ commitAllocated(path: string): Promise<ArtifactWriteResult>;
61
+ /** Release a failed allocation and remove any partial file. Idempotent. */
62
+ releaseAllocated(path: string): Promise<void>;
25
63
  }
26
64
  /**
27
65
  * Spill threshold in bytes. Content blocks longer than this are
@@ -1 +1 @@
1
- {"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/artifacts.ts"],"names":[],"mappings":"AA8BA,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,OAAO,CAAK;gBAER,YAAY,CAAC,EAAE,MAAM;IAQjC,mEAAmE;IACnE,IAAI,SAAS,IAAI,MAAM,CAEtB;YAEa,MAAM;IAOpB;;;;OAIG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAUlF;;;;OAIG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAMlD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAI/E"}
1
+ {"version":3,"file":"artifacts.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/artifacts.ts"],"names":[],"mappings":"AA6BA,OAAO,EAAE,gBAAgB,EAAa,MAAM,qBAAqB,CAAC;AAElE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,MAAM,CAAC;AACjC,eAAO,MAAM,eAAe,QAAoB,CAAC;AAEjD,qEAAqE;AACrE,qBAAa,kBAAmB,SAAQ,gBAAgB;gBAC1C,OAAO,EAAE,MAAM;CAM5B;AAED,MAAM,WAAW,mBAAmB;IAClC,sEAAsE;IACtE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,GAAG,CAAS;IACpB,OAAO,CAAC,KAAK,CAA8B;IAC3C,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;gBAE3B,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,GAAE,mBAAwB;IAUnE,mEAAmE;IACnE,IAAI,SAAS,IAAI,MAAM,CAEtB;YAEa,MAAM;IAOpB;;;;OAIG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAoBlF,OAAO,CAAC,iBAAiB;IAazB;;;;OAIG;IACG,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAmBjD;;;;;;OAMG;IACG,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsBjE,2EAA2E;IACrE,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAKpD;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AAExC,wBAAgB,iBAAiB,IAAI,MAAM,CAK1C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAI/E"}
@@ -24,19 +24,47 @@
24
24
  * reclaims them eventually. Override the root if you need a custom
25
25
  * cleanup policy.
26
26
  */
27
- import { mkdir, writeFile } from 'fs/promises';
27
+ import { chmod, mkdir, rm, stat, writeFile } from 'fs/promises';
28
28
  import { join, resolve } from 'path';
29
29
  import { tmpdir } from 'os';
30
+ import { CraftdriverError, ErrorCode } from '../../lib/errors.js';
31
+ /**
32
+ * Quota for one session's spill directory.
33
+ *
34
+ * The store is written to automatically, on every oversized result, for the
35
+ * whole life of a long-running session — and nothing deletes it. Without a
36
+ * quota a loop that snapshots a large page fills the disk silently, which is
37
+ * a worse failure than a refused spill: the caller falls back to an inline
38
+ * preview and the session keeps working.
39
+ */
40
+ export const MAX_ARTIFACTS = 500;
41
+ export const MAX_TOTAL_BYTES = 256 * 1024 * 1024;
42
+ /** Raised when a write would exceed the session's artifact quota. */
43
+ export class ArtifactQuotaError extends CraftdriverError {
44
+ constructor(message) {
45
+ super(ErrorCode.STATE_INVALID, message, {
46
+ hint: 'start a fresh MCP session, or clean the configured artifact root',
47
+ });
48
+ this.name = 'ArtifactQuotaError';
49
+ }
50
+ }
30
51
  export class ArtifactStore {
31
52
  dir;
32
53
  ready = null;
33
54
  counter = 0;
34
- constructor(rootOverride) {
55
+ artifactCount = 0;
56
+ totalBytes = 0;
57
+ reservations = new Set();
58
+ maxArtifacts;
59
+ maxTotalBytes;
60
+ constructor(rootOverride, limits = {}) {
35
61
  const root = rootOverride
36
62
  ?? process.env.CRAFTDRIVER_MCP_ARTIFACTS_DIR
37
63
  ?? tmpdir();
38
64
  const stamp = `${process.pid}-${Date.now().toString(36)}`;
39
65
  this.dir = resolve(root, `craftdriver-mcp-${stamp}`);
66
+ this.maxArtifacts = limits.maxArtifacts ?? MAX_ARTIFACTS;
67
+ this.maxTotalBytes = limits.maxTotalBytes ?? MAX_TOTAL_BYTES;
40
68
  }
41
69
  /** Absolute path to the artifact directory (may not exist yet). */
42
70
  get directory() {
@@ -44,7 +72,7 @@ export class ArtifactStore {
44
72
  }
45
73
  async ensure() {
46
74
  if (!this.ready) {
47
- this.ready = mkdir(this.dir, { recursive: true }).then(() => undefined);
75
+ this.ready = mkdir(this.dir, { recursive: true, mode: 0o700 }).then(() => undefined);
48
76
  }
49
77
  await this.ready;
50
78
  }
@@ -54,13 +82,32 @@ export class ArtifactStore {
54
82
  * `screenshot.png` or `eval.json`.
55
83
  */
56
84
  async write(nameHint, data) {
57
- await this.ensure();
85
+ const buf = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
86
+ this.assertWithinQuota(buf.length);
58
87
  this.counter += 1;
88
+ this.artifactCount += 1;
89
+ this.totalBytes += buf.length;
59
90
  const seq = this.counter.toString().padStart(4, '0');
60
91
  const path = join(this.dir, `${seq}-${nameHint}`);
61
- const buf = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
62
- await writeFile(path, buf);
63
- return { path, bytes: buf.length };
92
+ try {
93
+ await this.ensure();
94
+ await writeFile(path, buf, { mode: 0o600 });
95
+ return { path, bytes: buf.length };
96
+ }
97
+ catch (error) {
98
+ this.artifactCount -= 1;
99
+ this.totalBytes -= buf.length;
100
+ await rm(path, { force: true }).catch(() => { });
101
+ throw error;
102
+ }
103
+ }
104
+ assertWithinQuota(incoming) {
105
+ if (this.artifactCount >= this.maxArtifacts) {
106
+ throw new ArtifactQuotaError(`artifact store is full (${this.maxArtifacts} files in ${this.dir})`);
107
+ }
108
+ if (this.totalBytes + incoming > this.maxTotalBytes) {
109
+ throw new ArtifactQuotaError(`artifact store would exceed ${this.maxTotalBytes} bytes (${this.dir})`);
110
+ }
64
111
  }
65
112
  /**
66
113
  * Allocate a path for an artifact the caller will write itself (e.g.
@@ -68,10 +115,58 @@ export class ArtifactStore {
68
115
  * beyond `mkdir -p` of the directory.
69
116
  */
70
117
  async allocate(nameHint) {
71
- await this.ensure();
118
+ // Reserve the count synchronously, before awaiting mkdir, so concurrent
119
+ // allocations cannot all observe the same free slot.
120
+ this.assertWithinQuota(0);
72
121
  this.counter += 1;
122
+ this.artifactCount += 1;
73
123
  const seq = this.counter.toString().padStart(4, '0');
74
- return join(this.dir, `${seq}-${nameHint}`);
124
+ const path = join(this.dir, `${seq}-${nameHint}`);
125
+ this.reservations.add(path);
126
+ try {
127
+ await this.ensure();
128
+ return path;
129
+ }
130
+ catch (error) {
131
+ this.reservations.delete(path);
132
+ this.artifactCount -= 1;
133
+ throw error;
134
+ }
135
+ }
136
+ /**
137
+ * Account for a file written to a path returned by {@link allocate}.
138
+ *
139
+ * Screenshots are written by the browser, so their size is known only after
140
+ * capture. If the actual file would cross the quota, remove that one file and
141
+ * fail the tool call rather than letting the supposedly bounded store grow.
142
+ */
143
+ async commitAllocated(path) {
144
+ if (!this.reservations.has(path)) {
145
+ throw new Error(`artifact path was not allocated by this store: ${path}`);
146
+ }
147
+ try {
148
+ const info = await stat(path);
149
+ if (!info.isFile())
150
+ throw new Error(`allocated artifact is not a regular file: ${path}`);
151
+ if (this.totalBytes + info.size > this.maxTotalBytes) {
152
+ throw new ArtifactQuotaError(`artifact store would exceed ${this.maxTotalBytes} bytes (${this.dir})`);
153
+ }
154
+ await chmod(path, 0o600);
155
+ this.totalBytes += info.size;
156
+ this.reservations.delete(path);
157
+ return { path, bytes: info.size };
158
+ }
159
+ catch (error) {
160
+ await this.releaseAllocated(path);
161
+ throw error;
162
+ }
163
+ }
164
+ /** Release a failed allocation and remove any partial file. Idempotent. */
165
+ async releaseAllocated(path) {
166
+ if (!this.reservations.delete(path))
167
+ return;
168
+ this.artifactCount -= 1;
169
+ await rm(path, { force: true }).catch(() => { });
75
170
  }
76
171
  }
77
172
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../src/cli/mcp/artifacts.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"artifacts.js","sourceRoot":"","sources":["../../../src/cli/mcp/artifacts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Total response bounds for MCP tool results.
3
+ *
4
+ * Spilling a large `content` block to an artifact bounds what the *model*
5
+ * reads, but not what crosses the wire: the same value was still attached in
6
+ * full as `structuredContent`, so a 50 KB eval produced a 50,571-byte response
7
+ * with a 514-byte preview in front of it. Bounding one half of the payload
8
+ * while duplicating the other in full is worse than not bounding at all,
9
+ * because it looks bounded.
10
+ *
11
+ * So the cap here is on the **complete serialized response**, and the rules
12
+ * are:
13
+ *
14
+ * - **Never duplicate a large value.** If the preview already describes it,
15
+ * `structuredContent` carries metadata and a bounded preview, not the value.
16
+ * - **Truncation is always visible.** `truncated`, `totalBytes` and the
17
+ * retained byte count are part of the payload, so an agent can tell a short
18
+ * answer from a shortened one.
19
+ * - **Never emit invalid UTF-8.** Cutting at a byte offset can split a
20
+ * multi-byte sequence or a surrogate pair and produce a replacement
21
+ * character — or an unparseable frame. Truncation happens on code-point
22
+ * boundaries.
23
+ */
24
+ /** Cap on one complete serialized `tools/call` result. */
25
+ export declare const DEFAULT_MAX_RESPONSE_BYTES: number;
26
+ export declare function resolveMaxResponseBytes(env?: NodeJS.ProcessEnv): number;
27
+ export { utf8Bytes, truncateUtf8 } from '../bounds.js';
28
+ export interface BoundedStructured {
29
+ truncated: true;
30
+ /** Bytes the full value would have occupied. */
31
+ totalBytes: number;
32
+ /** Bytes actually retained in `preview`. */
33
+ previewBytes: number;
34
+ preview: string;
35
+ }
36
+ /**
37
+ * Replace an oversized structured value with a bounded, self-describing
38
+ * preview.
39
+ */
40
+ export declare function boundStructured(value: unknown, budgetBytes: number): BoundedStructured;
41
+ /**
42
+ * The minimum shape this needs. Kept structural and generic so the caller's
43
+ * richer `ContentBlock`/`ToolCallResult` types pass through unchanged rather
44
+ * than being widened to a local copy.
45
+ */
46
+ interface BoundableResult {
47
+ content: Array<{
48
+ type: string;
49
+ text?: string;
50
+ }>;
51
+ isError?: boolean;
52
+ structuredContent?: unknown;
53
+ }
54
+ /**
55
+ * Bound a complete tool result to `maxBytes` of serialized JSON.
56
+ *
57
+ * Order matters. `structuredContent` is reduced first because `content` is
58
+ * what the agent actually reads and has usually already been spilled to an
59
+ * artifact with a path in it — dropping that first would take away the pointer
60
+ * to the full data while keeping the bulk. Only if the result is still over
61
+ * budget are content blocks trimmed, newest-last, each keeping a marker so a
62
+ * shortened block never reads as a complete one.
63
+ */
64
+ export declare function boundToolResult<T extends BoundableResult>(result: T, maxBytes: number): T;
65
+ //# sourceMappingURL=bounds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.d.ts","sourceRoot":"","sources":["../../../src/cli/mcp/bounds.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAIH,0DAA0D;AAC1D,eAAO,MAAM,0BAA0B,QAAY,CAAC;AAEpD,wBAAgB,uBAAuB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAOpF;AAKD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAcvD,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,IAAI,CAAC;IAChB,gDAAgD;IAChD,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,iBAAiB,CActF;AAED;;;;GAIG;AACH,UAAU,eAAe;IACvB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAChD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,CAAC,SAAS,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,CAAC,CA0CzF"}
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Total response bounds for MCP tool results.
3
+ *
4
+ * Spilling a large `content` block to an artifact bounds what the *model*
5
+ * reads, but not what crosses the wire: the same value was still attached in
6
+ * full as `structuredContent`, so a 50 KB eval produced a 50,571-byte response
7
+ * with a 514-byte preview in front of it. Bounding one half of the payload
8
+ * while duplicating the other in full is worse than not bounding at all,
9
+ * because it looks bounded.
10
+ *
11
+ * So the cap here is on the **complete serialized response**, and the rules
12
+ * are:
13
+ *
14
+ * - **Never duplicate a large value.** If the preview already describes it,
15
+ * `structuredContent` carries metadata and a bounded preview, not the value.
16
+ * - **Truncation is always visible.** `truncated`, `totalBytes` and the
17
+ * retained byte count are part of the payload, so an agent can tell a short
18
+ * answer from a shortened one.
19
+ * - **Never emit invalid UTF-8.** Cutting at a byte offset can split a
20
+ * multi-byte sequence or a surrogate pair and produce a replacement
21
+ * character — or an unparseable frame. Truncation happens on code-point
22
+ * boundaries.
23
+ */
24
+ import { utf8Bytes, truncateUtf8 } from '../bounds.js';
25
+ /** Cap on one complete serialized `tools/call` result. */
26
+ export const DEFAULT_MAX_RESPONSE_BYTES = 32 * 1024;
27
+ export function resolveMaxResponseBytes(env = process.env) {
28
+ const raw = env.CRAFTDRIVER_MCP_MAX_RESPONSE_BYTES;
29
+ if (!raw)
30
+ return DEFAULT_MAX_RESPONSE_BYTES;
31
+ const n = Number.parseInt(raw, 10);
32
+ // A cap below the smallest useful envelope would make every response a
33
+ // truncation notice, so ignore nonsense rather than honouring it.
34
+ return Number.isFinite(n) && n >= 1024 ? n : DEFAULT_MAX_RESPONSE_BYTES;
35
+ }
36
+ // The byte-counting and code-point-safe truncation primitives are shared with
37
+ // the transport-neutral bound in `../bounds.js`; re-exported here so the MCP
38
+ // module stays the single import for callers working on its envelope.
39
+ export { utf8Bytes, truncateUtf8 } from '../bounds.js';
40
+ /** Serialized size of a value as it would appear in the response. */
41
+ function serializedBytes(value) {
42
+ if (value === undefined)
43
+ return 0;
44
+ try {
45
+ return utf8Bytes(JSON.stringify(value) ?? '');
46
+ }
47
+ catch {
48
+ // Circular or otherwise unserializable: treat as over budget so the
49
+ // caller replaces it rather than throwing while building a response.
50
+ return Number.POSITIVE_INFINITY;
51
+ }
52
+ }
53
+ /**
54
+ * Replace an oversized structured value with a bounded, self-describing
55
+ * preview.
56
+ */
57
+ export function boundStructured(value, budgetBytes) {
58
+ let serialized;
59
+ try {
60
+ serialized = JSON.stringify(value) ?? String(value);
61
+ }
62
+ catch {
63
+ serialized = String(value);
64
+ }
65
+ const preview = truncateUtf8(serialized, Math.max(budgetBytes, 0));
66
+ return {
67
+ truncated: true,
68
+ totalBytes: utf8Bytes(serialized),
69
+ previewBytes: utf8Bytes(preview),
70
+ preview,
71
+ };
72
+ }
73
+ /**
74
+ * Bound a complete tool result to `maxBytes` of serialized JSON.
75
+ *
76
+ * Order matters. `structuredContent` is reduced first because `content` is
77
+ * what the agent actually reads and has usually already been spilled to an
78
+ * artifact with a path in it — dropping that first would take away the pointer
79
+ * to the full data while keeping the bulk. Only if the result is still over
80
+ * budget are content blocks trimmed, newest-last, each keeping a marker so a
81
+ * shortened block never reads as a complete one.
82
+ */
83
+ export function boundToolResult(result, maxBytes) {
84
+ if (serializedBytes(result) <= maxBytes)
85
+ return result;
86
+ const bounded = { ...result };
87
+ if (bounded.structuredContent !== undefined) {
88
+ // Leave room for the content blocks and the envelope; a quarter of the
89
+ // budget is enough for a preview to be useful without crowding out the
90
+ // text the agent reads.
91
+ const structuredBudget = Math.floor(maxBytes / 4);
92
+ bounded.structuredContent = boundStructured(bounded.structuredContent, structuredBudget);
93
+ }
94
+ if (serializedBytes(bounded) <= maxBytes)
95
+ return bounded;
96
+ // Still over: trim the text blocks. Work from the last block backwards, so
97
+ // the primary result survives longer than a trailing snapshot diff.
98
+ const blocks = bounded.content.map((block) => ({ ...block }));
99
+ const marker = '\n… (truncated)';
100
+ const over = () => serializedBytes({ ...bounded, content: blocks }) - maxBytes;
101
+ for (let i = blocks.length - 1; i >= 0; i--) {
102
+ if (over() <= 0)
103
+ break;
104
+ const block = blocks[i];
105
+ if (typeof block.text !== 'string')
106
+ continue;
107
+ const original = block.text;
108
+ let keep = Math.max(0, utf8Bytes(original) - over() - utf8Bytes(marker));
109
+ // Converge rather than assuming one pass lands under the cap: JSON
110
+ // escaping makes the serialized size exceed the raw byte count (a newline
111
+ // is one byte but two characters once escaped), so a single subtraction
112
+ // reliably overshoots by a little.
113
+ for (let guard = 0; guard < 8; guard++) {
114
+ block.text = keep > 0 ? truncateUtf8(original, keep) + marker : marker;
115
+ const excess = over();
116
+ if (excess <= 0 || keep === 0)
117
+ break;
118
+ keep = Math.max(0, keep - excess);
119
+ }
120
+ }
121
+ bounded.content = blocks;
122
+ return bounded;
123
+ }
124
+ //# sourceMappingURL=bounds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bounds.js","sourceRoot":"","sources":["../../../src/cli/mcp/bounds.ts"],"names":[],"mappings":"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"}