@arnilo/prism-tools 0.0.0-stage → 0.13.1

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 (331) hide show
  1. package/CHANGELOG.md +418 -0
  2. package/LICENSE +21 -0
  3. package/README.md +44 -2
  4. package/browser/README.md +98 -0
  5. package/browser/skills/browser-automation/SKILL.md +14 -0
  6. package/browser/src/actions/cdp.ts +131 -0
  7. package/browser/src/actions/dialog.ts +25 -0
  8. package/browser/src/actions/download_release.ts +22 -0
  9. package/browser/src/actions/index.ts +37 -0
  10. package/browser/src/actions/locator.ts +70 -0
  11. package/browser/src/actions/navigate.ts +23 -0
  12. package/browser/src/actions/screenshot.ts +27 -0
  13. package/browser/src/actions/scroll.ts +24 -0
  14. package/browser/src/actions/select_page.ts +21 -0
  15. package/browser/src/actions/types.ts +114 -0
  16. package/browser/src/actions/upload.ts +44 -0
  17. package/browser/src/actions/wait.ts +45 -0
  18. package/browser/src/browser.ts +197 -0
  19. package/browser/src/cdp.ts +178 -0
  20. package/browser/src/checkpoint.ts +189 -0
  21. package/browser/src/config.ts +72 -0
  22. package/browser/src/downloads.ts +253 -0
  23. package/browser/src/errors.ts +31 -0
  24. package/browser/src/evaluate.ts +101 -0
  25. package/browser/src/index.ts +129 -0
  26. package/browser/src/limits.ts +271 -0
  27. package/browser/src/manager.ts +730 -0
  28. package/browser/src/network.ts +263 -0
  29. package/browser/src/observe.ts +178 -0
  30. package/browser/src/policy.ts +133 -0
  31. package/browser/src/screenshot.ts +107 -0
  32. package/browser/src/shared-sandbox.ts +53 -0
  33. package/browser/src/snapshot.ts +99 -0
  34. package/browser/src/targets.ts +165 -0
  35. package/browser/src/tools.ts +480 -0
  36. package/browser/src/types.ts +316 -0
  37. package/browser/src/uploads.ts +174 -0
  38. package/coding/README.md +337 -0
  39. package/coding/src/agent/acp-operations.ts +146 -0
  40. package/coding/src/agent/artifacts.ts +38 -0
  41. package/coding/src/agent/ask-user-decision.ts +623 -0
  42. package/coding/src/agent/atomic-write.ts +23 -0
  43. package/coding/src/agent/bounded-file.ts +21 -0
  44. package/coding/src/agent/checks.ts +301 -0
  45. package/coding/src/agent/coding-checkpoint.ts +828 -0
  46. package/coding/src/agent/delete.ts +237 -0
  47. package/coding/src/agent/diagnostics-tool.ts +163 -0
  48. package/coding/src/agent/diagnostics.ts +262 -0
  49. package/coding/src/agent/edit-diff.ts +382 -0
  50. package/coding/src/agent/edit.ts +322 -0
  51. package/coding/src/agent/effects.ts +102 -0
  52. package/coding/src/agent/env.ts +28 -0
  53. package/coding/src/agent/execution-policy.ts +89 -0
  54. package/coding/src/agent/file-mutation-queue.ts +72 -0
  55. package/coding/src/agent/forge/github.ts +603 -0
  56. package/coding/src/agent/forge/index.ts +16 -0
  57. package/coding/src/agent/forge/types.ts +185 -0
  58. package/coding/src/agent/git-aware-repository.ts +305 -0
  59. package/coding/src/agent/git-exec.ts +326 -0
  60. package/coding/src/agent/git-status.ts +177 -0
  61. package/coding/src/agent/git-tools.ts +360 -0
  62. package/coding/src/agent/git.ts +1074 -0
  63. package/coding/src/agent/glob-match.ts +146 -0
  64. package/coding/src/agent/glob.ts +182 -0
  65. package/coding/src/agent/goal-verify.ts +385 -0
  66. package/coding/src/agent/grep-worker.ts +74 -0
  67. package/coding/src/agent/grep.ts +459 -0
  68. package/coding/src/agent/identity.ts +48 -0
  69. package/coding/src/agent/index.ts +676 -0
  70. package/coding/src/agent/language/client.ts +429 -0
  71. package/coding/src/agent/language/framing.ts +190 -0
  72. package/coding/src/agent/language/index.ts +23 -0
  73. package/coding/src/agent/language/intelligence.ts +682 -0
  74. package/coding/src/agent/language/types.ts +187 -0
  75. package/coding/src/agent/lifecycle-events.ts +121 -0
  76. package/coding/src/agent/lifecycle.ts +166 -0
  77. package/coding/src/agent/limits.ts +214 -0
  78. package/coding/src/agent/move.ts +178 -0
  79. package/coding/src/agent/mutation-path.ts +52 -0
  80. package/coding/src/agent/output-accumulator.ts +304 -0
  81. package/coding/src/agent/path-utils.ts +120 -0
  82. package/coding/src/agent/process/index.ts +58 -0
  83. package/coding/src/agent/process/recovery.ts +508 -0
  84. package/coding/src/agent/process/sessions-host.ts +222 -0
  85. package/coding/src/agent/process/sessions-monitor.ts +141 -0
  86. package/coding/src/agent/process/sessions-recovery.ts +241 -0
  87. package/coding/src/agent/process/sessions-spawn.ts +613 -0
  88. package/coding/src/agent/process/sessions-teardown.ts +281 -0
  89. package/coding/src/agent/process/sessions.ts +30 -0
  90. package/coding/src/agent/process/types.ts +358 -0
  91. package/coding/src/agent/read-path-set.ts +126 -0
  92. package/coding/src/agent/read.ts +713 -0
  93. package/coding/src/agent/repository/glob.ts +147 -0
  94. package/coding/src/agent/repository/indexed-search.ts +480 -0
  95. package/coding/src/agent/repository/list.ts +131 -0
  96. package/coding/src/agent/repository/operations.ts +22 -0
  97. package/coding/src/agent/repository/path.ts +98 -0
  98. package/coding/src/agent/repository/search.ts +277 -0
  99. package/coding/src/agent/repository/types.ts +243 -0
  100. package/coding/src/agent/repository/walk.ts +126 -0
  101. package/coding/src/agent/repository.ts +12 -0
  102. package/coding/src/agent/shell-background.ts +264 -0
  103. package/coding/src/agent/shell-output.ts +75 -0
  104. package/coding/src/agent/shell.ts +526 -0
  105. package/coding/src/agent/todo.ts +399 -0
  106. package/coding/src/agent/truncate.ts +251 -0
  107. package/coding/src/agent/write.ts +170 -0
  108. package/coding/src/ask-user-host.ts +48 -0
  109. package/coding/src/config.ts +278 -0
  110. package/coding/src/hard-deny.ts +97 -0
  111. package/coding/src/index.ts +318 -0
  112. package/coding/src/principal-guard.ts +84 -0
  113. package/coding/src/sandbox.ts +92 -0
  114. package/coding/src/security/approval.ts +144 -0
  115. package/coding/src/security/command-rules.ts +57 -0
  116. package/coding/src/security/docker-cli.ts +222 -0
  117. package/coding/src/security/docker-sandbox.ts +1205 -0
  118. package/coding/src/security/e2b-sandbox.ts +853 -0
  119. package/coding/src/security/egress/dns-pin.ts +88 -0
  120. package/coding/src/security/egress/index.ts +32 -0
  121. package/coding/src/security/egress/limits.ts +57 -0
  122. package/coding/src/security/egress/policy.ts +137 -0
  123. package/coding/src/security/egress/proxy.ts +599 -0
  124. package/coding/src/security/egress/types.ts +30 -0
  125. package/coding/src/security/index.ts +199 -0
  126. package/coding/src/security/native-sandbox.ts +639 -0
  127. package/coding/src/security/path-containment.ts +55 -0
  128. package/coding/src/security/sandbox-coding-operations.ts +392 -0
  129. package/coding/src/security/sandbox-fs-operations.ts +644 -0
  130. package/coding/src/security/sandbox-limits.ts +147 -0
  131. package/coding/src/security/sandbox-tar.ts +308 -0
  132. package/coding/src/security/sandbox.ts +179 -0
  133. package/coding/src/security/semaphore.ts +55 -0
  134. package/coding/src/targets.ts +110 -0
  135. package/coding/src/workspace/diff.ts +78 -0
  136. package/coding/src/workspace/errors.ts +29 -0
  137. package/coding/src/workspace/index.ts +5 -0
  138. package/coding/src/workspace/merge.ts +151 -0
  139. package/coding/src/workspace/routing.ts +76 -0
  140. package/coding/src/workspace/service.ts +584 -0
  141. package/computer-use/README.md +105 -0
  142. package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
  143. package/computer-use/src/classify.ts +53 -0
  144. package/computer-use/src/command.ts +29 -0
  145. package/computer-use/src/config.ts +69 -0
  146. package/computer-use/src/create.ts +170 -0
  147. package/computer-use/src/devices.ts +247 -0
  148. package/computer-use/src/env.ts +28 -0
  149. package/computer-use/src/index.ts +157 -0
  150. package/computer-use/src/skill.ts +21 -0
  151. package/openapi/README.md +97 -0
  152. package/openapi/src/compile.ts +386 -0
  153. package/openapi/src/config.ts +73 -0
  154. package/openapi/src/errors.ts +16 -0
  155. package/openapi/src/execute.ts +241 -0
  156. package/openapi/src/index.ts +124 -0
  157. package/openapi/src/limits.ts +63 -0
  158. package/openapi/src/specs.ts +86 -0
  159. package/openapi/src/tools.ts +60 -0
  160. package/openapi/src/types.ts +61 -0
  161. package/package.json +171 -4
  162. package/research/README.md +72 -0
  163. package/research/skills/research-report/SKILL.md +12 -0
  164. package/research/skills/research-scoping/SKILL.md +10 -0
  165. package/research/skills/research-synthesis/SKILL.md +10 -0
  166. package/research/skills/source-triage/SKILL.md +10 -0
  167. package/research/src/check.ts +71 -0
  168. package/research/src/config.ts +41 -0
  169. package/research/src/index.ts +147 -0
  170. package/research/src/ledger.ts +157 -0
  171. package/research/src/urls.ts +21 -0
  172. package/web/README.md +287 -0
  173. package/web/skills/web-research/SKILL.md +17 -0
  174. package/web/src/brave.ts +66 -0
  175. package/web/src/cache.ts +151 -0
  176. package/web/src/config.ts +210 -0
  177. package/web/src/controls.ts +128 -0
  178. package/web/src/domains.ts +114 -0
  179. package/web/src/exa.ts +60 -0
  180. package/web/src/firecrawl.ts +160 -0
  181. package/web/src/index.ts +387 -0
  182. package/web/src/kagi.ts +73 -0
  183. package/web/src/limits.ts +55 -0
  184. package/web/src/native/convert.ts +84 -0
  185. package/web/src/native/fetch.ts +226 -0
  186. package/web/src/normalize.ts +131 -0
  187. package/web/src/obscura/cdp.ts +201 -0
  188. package/web/src/obscura/classify.ts +35 -0
  189. package/web/src/obscura/cli.ts +254 -0
  190. package/web/src/obscura/errors.ts +8 -0
  191. package/web/src/obscura/index.ts +59 -0
  192. package/web/src/obscura/limits.ts +92 -0
  193. package/web/src/obscura/mcp.ts +104 -0
  194. package/web/src/obscura/playwright.ts +133 -0
  195. package/web/src/obscura/process.ts +235 -0
  196. package/web/src/obscura/search-profile.ts +73 -0
  197. package/web/src/obscura/types.ts +48 -0
  198. package/web/src/obscura/web.ts +419 -0
  199. package/web/src/prompt-fetch.ts +88 -0
  200. package/web/src/render.ts +75 -0
  201. package/web/src/searxng.ts +78 -0
  202. package/web/src/slice.ts +139 -0
  203. package/web/src/tavily.ts +77 -0
  204. package/web/src/tools.ts +182 -0
  205. package/web/src/transport.ts +310 -0
  206. package/web/src/types.ts +199 -0
  207. package/work/README.md +77 -0
  208. package/work/THIRD_PARTY_NOTICES +24 -0
  209. package/work/docling/ocr.py +150 -0
  210. package/work/sandbox/Dockerfile +36 -0
  211. package/work/sandbox/soffice.sh +12 -0
  212. package/work/src/command.ts +29 -0
  213. package/work/src/config.ts +138 -0
  214. package/work/src/connectors/cli.ts +343 -0
  215. package/work/src/connectors/drafts.ts +647 -0
  216. package/work/src/connectors/errors.ts +8 -0
  217. package/work/src/connectors/file-bytes.ts +149 -0
  218. package/work/src/connectors/google-workspace-http.ts +399 -0
  219. package/work/src/connectors/google-workspace.ts +465 -0
  220. package/work/src/connectors/gws-tools.ts +378 -0
  221. package/work/src/connectors/http.ts +123 -0
  222. package/work/src/connectors/idempotency.ts +235 -0
  223. package/work/src/connectors/identity.ts +34 -0
  224. package/work/src/connectors/index.ts +93 -0
  225. package/work/src/connectors/limits.ts +50 -0
  226. package/work/src/connectors/m365-tools.ts +308 -0
  227. package/work/src/connectors/microsoft365-http.ts +428 -0
  228. package/work/src/connectors/microsoft365.ts +402 -0
  229. package/work/src/connectors/normalize.ts +120 -0
  230. package/work/src/connectors/tool-helpers.ts +398 -0
  231. package/work/src/connectors/tools.ts +42 -0
  232. package/work/src/connectors/types.ts +399 -0
  233. package/work/src/diagrams/canonicalize.ts +166 -0
  234. package/work/src/diagrams/caps.ts +52 -0
  235. package/work/src/diagrams/embed.ts +340 -0
  236. package/work/src/diagrams/errors.ts +76 -0
  237. package/work/src/diagrams/index.ts +81 -0
  238. package/work/src/diagrams/messages.ts +150 -0
  239. package/work/src/diagrams/origin.ts +52 -0
  240. package/work/src/diagrams/telemetry.ts +34 -0
  241. package/work/src/diagrams/xml.ts +267 -0
  242. package/work/src/document-extraction/index.ts +563 -0
  243. package/work/src/document-reader/ambient.d.ts +2 -0
  244. package/work/src/document-reader/errors.ts +7 -0
  245. package/work/src/document-reader/index.ts +181 -0
  246. package/work/src/document-reader/mistral-ocr.ts +318 -0
  247. package/work/src/document-reader/parsers.ts +171 -0
  248. package/work/src/documents/caps.ts +135 -0
  249. package/work/src/documents/diff.ts +269 -0
  250. package/work/src/documents/errors.ts +44 -0
  251. package/work/src/documents/fidelity.ts +98 -0
  252. package/work/src/documents/generate.ts +96 -0
  253. package/work/src/documents/hash.ts +8 -0
  254. package/work/src/documents/index.ts +14 -0
  255. package/work/src/documents/model-schema.ts +639 -0
  256. package/work/src/documents/parse.ts +252 -0
  257. package/work/src/documents/patch-history.ts +81 -0
  258. package/work/src/documents/patch.ts +476 -0
  259. package/work/src/documents/preview-html.ts +245 -0
  260. package/work/src/documents/preview.ts +227 -0
  261. package/work/src/documents/telemetry.ts +36 -0
  262. package/work/src/documents/translate/docx.ts +292 -0
  263. package/work/src/documents/translate/pptx.ts +304 -0
  264. package/work/src/documents/translate/xlsx.ts +137 -0
  265. package/work/src/documents/types.ts +188 -0
  266. package/work/src/index.ts +311 -0
  267. package/work/src/runtime/worker-pool-entry.ts +102 -0
  268. package/work/src/runtime/worker-pool.ts +267 -0
  269. package/work/src/sandbox/composition.ts +212 -0
  270. package/work/src/sandbox/image.ts +6 -0
  271. package/work/src/sandbox/index.ts +14 -0
  272. package/work/src/sheets/caps.ts +68 -0
  273. package/work/src/sheets/csv.ts +351 -0
  274. package/work/src/sheets/decimal.ts +388 -0
  275. package/work/src/sheets/errors.ts +52 -0
  276. package/work/src/sheets/index.ts +62 -0
  277. package/work/src/sheets/inference.ts +378 -0
  278. package/work/src/sheets/telemetry.ts +34 -0
  279. package/work/src/sheets/types.ts +162 -0
  280. package/work/src/sheets/xlsx.ts +388 -0
  281. package/work/src/skills/index.ts +8 -0
  282. package/work/src/skills/load.ts +43 -0
  283. package/work/src/tools/filesystem.ts +21 -0
  284. package/work/src/tools/index.ts +1 -0
  285. package/work/src/tools/local-filesystem.ts +33 -0
  286. package/work/src/tools/office.ts +311 -0
  287. package/work/vendor/hermes-agent/LICENSE +21 -0
  288. package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
  289. package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
  290. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
  291. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
  292. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
  293. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
  294. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
  295. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
  296. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
  297. package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
  298. package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
  299. package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
  300. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
  301. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
  302. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
  303. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
  304. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
  305. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
  306. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
  307. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
  308. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
  309. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
  310. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
  311. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
  312. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
  313. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
  314. package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
  315. package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
  316. package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
  317. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
  318. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
  319. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
  320. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
  321. package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
  322. package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
  323. package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
  324. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
  325. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
  326. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
  327. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
  328. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
  329. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
  330. package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
  331. package/work/vendor-lock.json +11 -0
@@ -0,0 +1,178 @@
1
+ import { access, constants } from "node:fs";
2
+ import { lstat, rename, unlink } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { promisify } from "node:util";
5
+ import type { JsonObject, ToolDefinition, ToolExecutionContext, ToolResult } from "@arnilo/prism";
6
+ import type { MutationStat } from "./delete.js";
7
+ import { CODING_LOCAL_EFFECT } from "./effects.js";
8
+ import type { ExecutionPolicy } from "./execution-policy.js";
9
+ import { enforceExecutionPolicy } from "./execution-policy.js";
10
+ import { withFileMutationQueue } from "./file-mutation-queue.js";
11
+ import type { CodingLifecycleEvent } from "./lifecycle-events.js";
12
+ import { resolveContainedMutationPath } from "./mutation-path.js";
13
+
14
+ const accessAsync = promisify(access);
15
+
16
+ export interface MoveOperations {
17
+ lstat: (absolutePath: string, options?: { signal?: AbortSignal }) => Promise<MutationStat>;
18
+ rename: (from: string, to: string, options?: { signal?: AbortSignal }) => Promise<void>;
19
+ unlink: (absolutePath: string, options?: { signal?: AbortSignal }) => Promise<void>;
20
+ access: (absolutePath: string, options?: { signal?: AbortSignal }) => Promise<void>;
21
+ }
22
+
23
+ export interface MoveToolOptions {
24
+ executionPolicy?: ExecutionPolicy;
25
+ operations?: MoveOperations;
26
+ /** Optional consumer-gated lifecycle listener (file_changed / permission_denied). */
27
+ onEvent?: (event: CodingLifecycleEvent) => void;
28
+ }
29
+
30
+ const defaultMoveOperations: MoveOperations = {
31
+ lstat: async (path) => {
32
+ const st = await lstat(path);
33
+ return {
34
+ isFile: () => st.isFile(),
35
+ isDirectory: () => st.isDirectory(),
36
+ isSymbolicLink: () => st.isSymbolicLink(),
37
+ size: st.size,
38
+ };
39
+ },
40
+ rename: (from, to) => rename(from, to).then(() => {}),
41
+ unlink: (path) => unlink(path).then(() => {}),
42
+ access: (path) => accessAsync(path, constants.F_OK),
43
+ };
44
+
45
+ function errorResult(toolCallId: string, message: string): ToolResult {
46
+ return {
47
+ toolCallId,
48
+ name: "move",
49
+ content: [{ type: "text", text: message }],
50
+ error: { message },
51
+ };
52
+ }
53
+
54
+ async function withDualFileMutationQueue<T>(pathA: string, pathB: string, fn: () => Promise<T>): Promise<T> {
55
+ const [first, second] = pathA <= pathB ? [pathA, pathB] : [pathB, pathA];
56
+ return withFileMutationQueue(first, () => withFileMutationQueue(second, fn));
57
+ }
58
+
59
+ export function createMoveTool(cwd: string, options?: MoveToolOptions): ToolDefinition {
60
+ const ops = options?.operations ?? defaultMoveOperations;
61
+
62
+ return {
63
+ name: "move",
64
+ kind: "move",
65
+ effect: CODING_LOCAL_EFFECT,
66
+ effects: ["write"],
67
+ description:
68
+ "High-risk: move or rename a file within the workspace. Set overwrite=true to replace an existing destination file only (directories/non-empty dest rejected). Does not create parent directories. No trash — host undo is not automatic.",
69
+ parameters: {
70
+ type: "object",
71
+ properties: {
72
+ from: { type: "string", description: "Source path (relative or absolute)" },
73
+ to: { type: "string", description: "Destination path (relative or absolute)" },
74
+ overwrite: {
75
+ type: "boolean",
76
+ description: "Replace an existing destination file when true (default false).",
77
+ },
78
+ },
79
+ required: ["from", "to"],
80
+ additionalProperties: false,
81
+ } as JsonObject,
82
+ async execute(args, context: ToolExecutionContext): Promise<ToolResult> {
83
+ const toolCallId = context.toolCallId;
84
+ const from = typeof args.from === "string" ? args.from : "";
85
+ const to = typeof args.to === "string" ? args.to : "";
86
+ const overwrite = args.overwrite === true;
87
+
88
+ if (from.length === 0) return errorResult(toolCallId, "from is required and must be a non-empty string.");
89
+ if (to.length === 0) return errorResult(toolCallId, "to is required and must be a non-empty string.");
90
+
91
+ try {
92
+ let fromPath: string;
93
+ try {
94
+ fromPath = await resolveContainedMutationPath(cwd, from);
95
+ } catch (error) {
96
+ const err = error as NodeJS.ErrnoException;
97
+ if (err.code === "ENOENT") return errorResult(toolCallId, `Source does not exist: ${from}`);
98
+ throw error;
99
+ }
100
+ const toPath = await resolveContainedMutationPath(cwd, to, { allowMissing: true });
101
+
102
+ const policyCheck = await enforceExecutionPolicy(
103
+ options?.executionPolicy,
104
+ {
105
+ kind: "move",
106
+ operation: "move",
107
+ paths: [fromPath, toPath],
108
+ risk: "high",
109
+ metadata: { overwrite, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
110
+ },
111
+ toolCallId,
112
+ "move",
113
+ (denied) => options?.onEvent?.({ type: "permission_denied", ...denied }),
114
+ );
115
+ if (!policyCheck.allowed) return policyCheck.result;
116
+ const allowedFrom = policyCheck.action.paths?.[0] ?? fromPath;
117
+ const allowedTo = policyCheck.action.paths?.[1] ?? toPath;
118
+
119
+ return await withDualFileMutationQueue(allowedFrom, allowedTo, async () => {
120
+ if (context.signal?.aborted) return errorResult(toolCallId, "Operation aborted");
121
+
122
+ let fromStat: MutationStat;
123
+ try {
124
+ fromStat = await ops.lstat(allowedFrom, { signal: context.signal });
125
+ } catch (error) {
126
+ const err = error as NodeJS.ErrnoException;
127
+ if (err.code === "ENOENT") return errorResult(toolCallId, `Source does not exist: ${from}`);
128
+ const message = error instanceof Error ? error.message : String(error);
129
+ return errorResult(toolCallId, message);
130
+ }
131
+
132
+ const destParent = dirname(allowedTo);
133
+ try {
134
+ await ops.access(destParent, { signal: context.signal });
135
+ } catch {
136
+ return errorResult(toolCallId, `Destination parent directory does not exist: ${destParent}`);
137
+ }
138
+
139
+ let destStat: MutationStat | undefined;
140
+ try {
141
+ destStat = await ops.lstat(allowedTo, { signal: context.signal });
142
+ } catch (error) {
143
+ const err = error as NodeJS.ErrnoException;
144
+ if (err.code !== "ENOENT") {
145
+ const message = error instanceof Error ? error.message : String(error);
146
+ return errorResult(toolCallId, message);
147
+ }
148
+ }
149
+
150
+ if (destStat) {
151
+ if (!overwrite) return errorResult(toolCallId, `Destination already exists: ${to}. Pass overwrite=true to replace.`);
152
+ if (destStat.isDirectory()) return errorResult(toolCallId, `Destination is a directory: ${to}`);
153
+ await ops.unlink(allowedTo, { signal: context.signal });
154
+ }
155
+
156
+ if (context.signal?.aborted) return errorResult(toolCallId, "Operation aborted");
157
+ await ops.rename(allowedFrom, allowedTo, { signal: context.signal });
158
+ options?.onEvent?.({ type: "file_changed", path: allowedTo, op: "move", toolCallId });
159
+
160
+ return {
161
+ toolCallId,
162
+ name: "move",
163
+ content: [{ type: "text", text: `Successfully moved ${from} to ${allowedTo}` }],
164
+ metadata: {
165
+ from: allowedFrom,
166
+ to: allowedTo,
167
+ bytes: fromStat.isFile() ? fromStat.size : undefined,
168
+ overwrite,
169
+ },
170
+ };
171
+ });
172
+ } catch (error) {
173
+ const message = error instanceof Error ? error.message : String(error);
174
+ return errorResult(toolCallId, message);
175
+ }
176
+ },
177
+ };
178
+ }
@@ -0,0 +1,52 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { isAbsolute, relative, resolve } from "node:path";
3
+ import { resolveToCwd } from "./path-utils.js";
4
+
5
+ function isPathInsideRoot(root: string, target: string): boolean {
6
+ const from = resolve(root);
7
+ const to = resolve(target);
8
+ if (to === from) return true;
9
+ const rel = relative(from, to);
10
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
11
+ }
12
+
13
+ function isMissingPathError(error: unknown): boolean {
14
+ return (
15
+ typeof error === "object" &&
16
+ error !== null &&
17
+ "code" in error &&
18
+ ((error as NodeJS.ErrnoException).code === "ENOENT" || (error as NodeJS.ErrnoException).code === "ENOTDIR")
19
+ );
20
+ }
21
+
22
+ /**
23
+ * Resolve a mutation target under `root`. Symlink paths are kept as the link
24
+ * location (not followed). Non-symlink existing paths are realpath'd.
25
+ */
26
+ export async function resolveContainedMutationPath(root: string, inputPath: string, options?: { allowMissing?: boolean }): Promise<string> {
27
+ const rootResolved = resolve(root);
28
+ let rootReal: string;
29
+ try {
30
+ rootReal = await realpath(rootResolved);
31
+ } catch {
32
+ throw new Error(`workspace root is missing or unreadable: ${rootResolved}`);
33
+ }
34
+
35
+ const candidate = resolveToCwd(inputPath, rootReal);
36
+ if (!isPathInsideRoot(rootReal, candidate)) {
37
+ throw new Error(`path escapes workspace root: ${inputPath}`);
38
+ }
39
+
40
+ try {
41
+ const st = await lstat(candidate);
42
+ if (st.isSymbolicLink()) return candidate;
43
+ const real = await realpath(candidate);
44
+ if (!isPathInsideRoot(rootReal, real)) {
45
+ throw new Error(`path resolves outside workspace root: ${inputPath}`);
46
+ }
47
+ return real;
48
+ } catch (error) {
49
+ if (options?.allowMissing && isMissingPathError(error)) return candidate;
50
+ throw error;
51
+ }
52
+ }
@@ -0,0 +1,304 @@
1
+ /** Streaming UTF-8 output retention with bounded memory and spill storage. */
2
+ import { randomBytes } from "node:crypto";
3
+ import { closeSync, openSync, readSync, writeSync } from "node:fs";
4
+ import { rm } from "node:fs/promises";
5
+ import { tmpdir } from "node:os";
6
+ import { join } from "node:path";
7
+ import {
8
+ DEFAULT_MAX_BYTES,
9
+ DEFAULT_MAX_LINES,
10
+ DEFAULT_MAX_TOTAL_OUTPUT_BYTES,
11
+ HARD_MAX_BYTES,
12
+ HARD_MAX_LINES,
13
+ HARD_MAX_TOTAL_OUTPUT_BYTES,
14
+ validateCodingLimit,
15
+ } from "./limits.js";
16
+ import { type TruncationResult, truncateTail } from "./truncate.js";
17
+
18
+ export interface OutputAccumulatorOptions {
19
+ maxLines?: number;
20
+ maxBytes?: number;
21
+ maxTotalOutputBytes?: number;
22
+ tempFilePrefix?: string;
23
+ onLimit?: () => void;
24
+ onStorageError?: () => void;
25
+ }
26
+
27
+ export interface OutputSnapshot {
28
+ content: string;
29
+ truncation: TruncationResult;
30
+ fullOutputPath?: string;
31
+ }
32
+
33
+ function defaultTempFilePath(prefix: string): string {
34
+ return join(tmpdir(), `${prefix}-${randomBytes(16).toString("hex")}.log`);
35
+ }
36
+
37
+ function byteLength(text: string): number {
38
+ return Buffer.byteLength(text, "utf-8");
39
+ }
40
+
41
+ export class OutputAccumulator {
42
+ private readonly maxLines: number;
43
+ private readonly maxBytes: number;
44
+ private readonly maxTotalOutputBytes: number;
45
+ private readonly maxRollingBytes: number;
46
+ private readonly tempFilePrefix: string;
47
+ private readonly onLimit?: () => void;
48
+ private readonly onStorageError?: () => void;
49
+ private readonly decoder = new TextDecoder();
50
+ private rawChunks: Buffer[] = [];
51
+ private tailText = "";
52
+ private tailBytes = 0;
53
+ private tailStartsAtLineBoundary = true;
54
+ private totalRawBytes = 0;
55
+ private totalDecodedBytes = 0;
56
+ private completedLines = 0;
57
+ private totalLines = 0;
58
+ private currentLineBytes = 0;
59
+ private hasOpenLine = false;
60
+ private finished = false;
61
+ private exceeded = false;
62
+ private tempFilePath?: string;
63
+ private tempFileFd?: number;
64
+ private tempFileError?: Error;
65
+
66
+ constructor(options: OutputAccumulatorOptions = {}) {
67
+ this.maxLines = validateCodingLimit("maxLines", options.maxLines ?? DEFAULT_MAX_LINES, HARD_MAX_LINES);
68
+ this.maxBytes = validateCodingLimit("maxBytes", options.maxBytes ?? DEFAULT_MAX_BYTES, HARD_MAX_BYTES);
69
+ this.maxTotalOutputBytes = validateCodingLimit(
70
+ "maxTotalOutputBytes",
71
+ options.maxTotalOutputBytes ?? DEFAULT_MAX_TOTAL_OUTPUT_BYTES,
72
+ HARD_MAX_TOTAL_OUTPUT_BYTES,
73
+ );
74
+ if (this.maxTotalOutputBytes < this.maxBytes) {
75
+ throw new Error("maxTotalOutputBytes must be at least maxBytes");
76
+ }
77
+ this.maxRollingBytes = this.maxBytes * 2;
78
+ this.tempFilePrefix = options.tempFilePrefix ?? "prism-output";
79
+ if (!/^[A-Za-z0-9._-]+$/.test(this.tempFilePrefix)) {
80
+ throw new Error("tempFilePrefix may contain only letters, numbers, dot, underscore, and hyphen");
81
+ }
82
+ this.onLimit = options.onLimit;
83
+ this.onStorageError = options.onStorageError;
84
+ }
85
+
86
+ append(data: Buffer): boolean {
87
+ if (this.finished) throw new Error("Cannot append to a finished output accumulator");
88
+ const remaining = this.maxTotalOutputBytes - this.totalRawBytes;
89
+ const accepted = remaining > 0 ? data.subarray(0, remaining) : data.subarray(0, 0);
90
+ if (accepted.length > 0) {
91
+ this.totalRawBytes += accepted.length;
92
+ this.appendDecodedText(this.decoder.decode(accepted, { stream: true }));
93
+ if (this.tempFileFd !== undefined || this.shouldUseTempFile()) {
94
+ this.ensureTempFile();
95
+ this.writeTemp(accepted);
96
+ } else {
97
+ this.rawChunks.push(Buffer.from(accepted));
98
+ }
99
+ }
100
+ if (accepted.length !== data.length && !this.exceeded) {
101
+ this.exceeded = true;
102
+ this.onLimit?.();
103
+ }
104
+ return !this.exceeded;
105
+ }
106
+
107
+ finish(): void {
108
+ if (this.finished) return;
109
+ this.finished = true;
110
+ this.appendDecodedText(this.decoder.decode());
111
+ if (this.shouldUseTempFile()) this.ensureTempFile();
112
+ }
113
+
114
+ snapshot(options: { persistIfTruncated?: boolean } = {}): OutputSnapshot {
115
+ const tailTruncation = truncateTail(this.getSnapshotText(), {
116
+ maxLines: this.maxLines,
117
+ maxBytes: this.maxBytes,
118
+ });
119
+ const truncated = this.totalLines > this.maxLines || this.totalDecodedBytes > this.maxBytes || this.exceeded;
120
+ const truncation: TruncationResult = {
121
+ ...tailTruncation,
122
+ truncated,
123
+ truncatedBy: truncated
124
+ ? (tailTruncation.truncatedBy ?? (this.totalDecodedBytes > this.maxBytes || this.exceeded ? "bytes" : "lines"))
125
+ : null,
126
+ totalLines: this.totalLines,
127
+ totalLinesKnown: true,
128
+ totalBytes: this.totalDecodedBytes,
129
+ totalBytesKnown: !this.exceeded,
130
+ maxLines: this.maxLines,
131
+ maxBytes: this.maxBytes,
132
+ };
133
+ if (options.persistIfTruncated && truncation.truncated) this.ensureTempFile();
134
+ return {
135
+ content: truncation.content,
136
+ truncation,
137
+ fullOutputPath: this.tempFileError ? undefined : this.tempFilePath,
138
+ };
139
+ }
140
+
141
+ async closeTempFile(): Promise<void> {
142
+ if (this.tempFileFd !== undefined) {
143
+ const fd = this.tempFileFd;
144
+ this.tempFileFd = undefined;
145
+ try {
146
+ closeSync(fd);
147
+ } catch (error) {
148
+ this.recordTempError(error);
149
+ }
150
+ }
151
+ if (this.tempFileError) throw this.tempFileError;
152
+ }
153
+
154
+ async cleanupTempFile(): Promise<void> {
155
+ const path = this.tempFilePath;
156
+ let closeError: unknown;
157
+ try {
158
+ await this.closeTempFile();
159
+ } catch (error) {
160
+ closeError = error;
161
+ }
162
+ this.tempFilePath = undefined;
163
+ this.tempFileError = undefined;
164
+ if (path) await rm(path, { force: true });
165
+ if (closeError) throw closeError;
166
+ }
167
+
168
+ getLastLineBytes(): number {
169
+ return this.currentLineBytes;
170
+ }
171
+
172
+ getTotalRawBytes(): number {
173
+ return this.totalRawBytes;
174
+ }
175
+
176
+ /**
177
+ * Cursor-paged raw bytes for process sessions.
178
+ * Reads from spill file when present, otherwise from in-memory chunks.
179
+ */
180
+ readRaw(cursor: number, maxBytes: number): { readonly data: Buffer; readonly nextCursor: number } {
181
+ if (!Number.isSafeInteger(cursor) || cursor < 0) {
182
+ throw new Error("cursor must be a non-negative safe integer");
183
+ }
184
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1) {
185
+ throw new Error("maxBytes must be a positive safe integer");
186
+ }
187
+ const total = this.totalRawBytes;
188
+ if (cursor >= total) return { data: Buffer.alloc(0), nextCursor: cursor };
189
+ const length = Math.min(maxBytes, total - cursor);
190
+ if (this.tempFilePath) {
191
+ const fd = openSync(this.tempFilePath, "r");
192
+ try {
193
+ const data = Buffer.alloc(length);
194
+ const n = readSync(fd, data, 0, length, cursor);
195
+ return { data: n === length ? data : data.subarray(0, n), nextCursor: cursor + n };
196
+ } finally {
197
+ closeSync(fd);
198
+ }
199
+ }
200
+ const data = Buffer.alloc(length);
201
+ let offset = 0;
202
+ let skipped = 0;
203
+ for (const chunk of this.rawChunks) {
204
+ if (offset >= length) break;
205
+ if (skipped + chunk.length <= cursor) {
206
+ skipped += chunk.length;
207
+ continue;
208
+ }
209
+ const start = Math.max(0, cursor - skipped);
210
+ const take = Math.min(chunk.length - start, length - offset);
211
+ chunk.copy(data, offset, start, start + take);
212
+ offset += take;
213
+ skipped += chunk.length;
214
+ }
215
+ return { data: offset === length ? data : data.subarray(0, offset), nextCursor: cursor + offset };
216
+ }
217
+
218
+ isOutputLimitExceeded(): boolean {
219
+ return this.exceeded;
220
+ }
221
+
222
+ hasStorageError(): boolean {
223
+ return this.tempFileError !== undefined;
224
+ }
225
+
226
+ private appendDecodedText(text: string): void {
227
+ if (text.length === 0) return;
228
+ const bytes = byteLength(text);
229
+ this.totalDecodedBytes += bytes;
230
+ this.tailText += text;
231
+ this.tailBytes += bytes;
232
+ if (this.tailBytes > this.maxRollingBytes * 2) this.trimTail();
233
+ let newlines = 0;
234
+ let lastNewline = -1;
235
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1)) {
236
+ newlines++;
237
+ lastNewline = i;
238
+ }
239
+ if (newlines === 0) {
240
+ this.currentLineBytes += bytes;
241
+ this.hasOpenLine = true;
242
+ } else {
243
+ this.completedLines += newlines;
244
+ const tail = text.slice(lastNewline + 1);
245
+ this.currentLineBytes = byteLength(tail);
246
+ this.hasOpenLine = tail.length > 0;
247
+ }
248
+ this.totalLines = this.completedLines + (this.hasOpenLine ? 1 : 0);
249
+ }
250
+
251
+ private trimTail(): void {
252
+ const buffer = Buffer.from(this.tailText, "utf-8");
253
+ if (buffer.length <= this.maxRollingBytes) {
254
+ this.tailBytes = buffer.length;
255
+ return;
256
+ }
257
+ let start = buffer.length - this.maxRollingBytes;
258
+ while (start < buffer.length && (buffer[start] & 0xc0) === 0x80) start++;
259
+ this.tailStartsAtLineBoundary = start === 0 ? this.tailStartsAtLineBoundary : buffer[start - 1] === 0x0a;
260
+ this.tailText = buffer.subarray(start).toString("utf-8");
261
+ this.tailBytes = byteLength(this.tailText);
262
+ }
263
+
264
+ private getSnapshotText(): string {
265
+ if (this.tailStartsAtLineBoundary) return this.tailText;
266
+ const firstNewline = this.tailText.indexOf("\n");
267
+ return firstNewline === -1 ? this.tailText : this.tailText.slice(firstNewline + 1);
268
+ }
269
+
270
+ private shouldUseTempFile(): boolean {
271
+ return this.totalRawBytes > this.maxBytes || this.totalDecodedBytes > this.maxBytes || this.totalLines > this.maxLines;
272
+ }
273
+
274
+ private ensureTempFile(): void {
275
+ if (this.tempFilePath || this.tempFileError) return;
276
+ const path = defaultTempFilePath(this.tempFilePrefix);
277
+ try {
278
+ this.tempFileFd = openSync(path, "wx", 0o600);
279
+ this.tempFilePath = path;
280
+ for (const chunk of this.rawChunks) this.writeTemp(chunk);
281
+ } catch (error) {
282
+ this.recordTempError(error);
283
+ } finally {
284
+ this.rawChunks = [];
285
+ }
286
+ }
287
+
288
+ private writeTemp(data: Buffer): void {
289
+ // ponytail: synchronous spill is zero-queue backpressure; use a bounded async writer only if measured throughput requires it.
290
+ if (this.tempFileFd === undefined || this.tempFileError) return;
291
+ try {
292
+ let offset = 0;
293
+ while (offset < data.length) offset += writeSync(this.tempFileFd, data, offset);
294
+ } catch (error) {
295
+ this.recordTempError(error);
296
+ }
297
+ }
298
+
299
+ private recordTempError(error: unknown): void {
300
+ if (this.tempFileError) return;
301
+ this.tempFileError = error instanceof Error ? error : new Error(String(error));
302
+ this.onStorageError?.();
303
+ }
304
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Path resolution helpers.
3
+ *
4
+ * Behavioral port of pi's core/tools/path-utils for @arnilo/prism-tools/coding.
5
+ * stdlib only (node:fs, node:os, node:path, node:url). pi's version delegates
6
+ * homedir/tilde expansion to an internal utils/paths.js; that logic is inlined
7
+ * here as normalizePath/resolvePath so the package stays self-contained.
8
+ */
9
+ import { accessSync, constants as fsConstants } from "node:fs";
10
+ import { access } from "node:fs/promises";
11
+ import { homedir } from "node:os";
12
+ import { isAbsolute, join, resolve as nodeResolve } from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+
15
+ const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
16
+ const NARROW_NO_BREAK_SPACE = "\u202F";
17
+
18
+ interface NormalizePathOptions {
19
+ trim?: boolean;
20
+ normalizeUnicodeSpaces?: boolean;
21
+ stripAtPrefix?: boolean;
22
+ expandTilde?: boolean;
23
+ homeDir?: string;
24
+ }
25
+
26
+ function normalizePath(input: string, options: NormalizePathOptions = {}): string {
27
+ let normalized = options.trim ? input.trim() : input;
28
+ if (options.normalizeUnicodeSpaces) {
29
+ normalized = normalized.replace(UNICODE_SPACES, " ");
30
+ }
31
+ if (options.stripAtPrefix && normalized.startsWith("@")) {
32
+ normalized = normalized.slice(1);
33
+ }
34
+ if (options.expandTilde ?? true) {
35
+ const home = options.homeDir ?? homedir();
36
+ if (normalized === "~") return home;
37
+ if (normalized.startsWith("~/") || (process.platform === "win32" && normalized.startsWith("~\\"))) {
38
+ return join(home, normalized.slice(2));
39
+ }
40
+ }
41
+ if (/^file:\/\//.test(normalized)) {
42
+ return fileURLToPath(normalized);
43
+ }
44
+ return normalized;
45
+ }
46
+
47
+ function resolvePath(input: string, baseDir: string = process.cwd(), options: NormalizePathOptions = {}): string {
48
+ const normalized = normalizePath(input, options);
49
+ const normalizedBaseDir = normalizePath(baseDir);
50
+ return isAbsolute(normalized) ? nodeResolve(normalized) : nodeResolve(normalizedBaseDir, normalized);
51
+ }
52
+
53
+ export async function pathExists(filePath: string): Promise<boolean> {
54
+ try {
55
+ await access(filePath, fsConstants.F_OK);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ export function expandPath(filePath: string): string {
63
+ return normalizePath(filePath, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
64
+ }
65
+
66
+ /**
67
+ * Resolve a path relative to the given cwd. Handles ~ expansion and absolute paths.
68
+ */
69
+ export function resolveToCwd(filePath: string, cwd: string): string {
70
+ return resolvePath(filePath, cwd, { normalizeUnicodeSpaces: true, stripAtPrefix: true });
71
+ }
72
+
73
+ // macOS screenshot / NFD / curly-quote fallbacks for read paths.
74
+ function tryMacOSScreenshotPath(filePath: string): string {
75
+ return filePath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`);
76
+ }
77
+ function tryNFDVariant(filePath: string): string {
78
+ // macOS stores filenames in NFD (decomposed) form; try converting user input to NFD.
79
+ return filePath.normalize("NFD");
80
+ }
81
+ function tryCurlyQuoteVariant(filePath: string): string {
82
+ // macOS uses U+2019 in screenshot names; users typically type a straight apostrophe.
83
+ return filePath.replace(/'/g, "\u2019");
84
+ }
85
+ function fileExists(filePath: string): boolean {
86
+ try {
87
+ accessSync(filePath, fsConstants.F_OK);
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ export function resolveReadPath(filePath: string, cwd: string): string {
95
+ const resolved = resolveToCwd(filePath, cwd);
96
+ if (fileExists(resolved)) return resolved;
97
+ const amPmVariant = tryMacOSScreenshotPath(resolved);
98
+ if (amPmVariant !== resolved && fileExists(amPmVariant)) return amPmVariant;
99
+ const nfdVariant = tryNFDVariant(resolved);
100
+ if (nfdVariant !== resolved && fileExists(nfdVariant)) return nfdVariant;
101
+ const curlyVariant = tryCurlyQuoteVariant(resolved);
102
+ if (curlyVariant !== resolved && fileExists(curlyVariant)) return curlyVariant;
103
+ const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
104
+ if (nfdCurlyVariant !== resolved && fileExists(nfdCurlyVariant)) return nfdCurlyVariant;
105
+ return resolved;
106
+ }
107
+
108
+ export async function resolveReadPathAsync(filePath: string, cwd: string): Promise<string> {
109
+ const resolved = resolveToCwd(filePath, cwd);
110
+ if (await pathExists(resolved)) return resolved;
111
+ const amPmVariant = tryMacOSScreenshotPath(resolved);
112
+ if (amPmVariant !== resolved && (await pathExists(amPmVariant))) return amPmVariant;
113
+ const nfdVariant = tryNFDVariant(resolved);
114
+ if (nfdVariant !== resolved && (await pathExists(nfdVariant))) return nfdVariant;
115
+ const curlyVariant = tryCurlyQuoteVariant(resolved);
116
+ if (curlyVariant !== resolved && (await pathExists(curlyVariant))) return curlyVariant;
117
+ const nfdCurlyVariant = tryCurlyQuoteVariant(nfdVariant);
118
+ if (nfdCurlyVariant !== resolved && (await pathExists(nfdCurlyVariant))) return nfdCurlyVariant;
119
+ return resolved;
120
+ }