@ilya-lesikov/pi-pi 0.1.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 (271) hide show
  1. package/3p/pi-ask-user/index.ts +1835 -0
  2. package/3p/pi-ask-user/package.json +50 -0
  3. package/3p/pi-ask-user/single-select-layout.ts +203 -0
  4. package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
  5. package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
  6. package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
  7. package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
  8. package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
  9. package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
  10. package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
  11. package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
  12. package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
  13. package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
  14. package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
  15. package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
  16. package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
  17. package/3p/pi-lsp/package.json +54 -0
  18. package/3p/pi-plannotator/AGENTS.md +583 -0
  19. package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
  20. package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
  21. package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
  22. package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
  23. package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
  24. package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
  25. package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
  26. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
  27. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
  28. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
  29. package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
  30. package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
  31. package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
  32. package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
  33. package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
  34. package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
  35. package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
  36. package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
  37. package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
  38. package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
  39. package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
  40. package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
  41. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
  42. package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
  43. package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
  44. package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
  45. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
  46. package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
  47. package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
  48. package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
  49. package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
  50. package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
  51. package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
  52. package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
  53. package/3p/pi-plannotator/package.json +53 -0
  54. package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
  55. package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
  56. package/3p/pi-plannotator/packages/ai/context.ts +250 -0
  57. package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
  58. package/3p/pi-plannotator/packages/ai/index.ts +105 -0
  59. package/3p/pi-plannotator/packages/ai/package.json +21 -0
  60. package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
  61. package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
  62. package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
  63. package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
  64. package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
  65. package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
  66. package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
  67. package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
  68. package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
  69. package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
  70. package/3p/pi-plannotator/packages/ai/types.ts +379 -0
  71. package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
  72. package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
  73. package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
  74. package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
  75. package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
  76. package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
  77. package/3p/pi-plannotator/packages/server/browser.ts +245 -0
  78. package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
  79. package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
  80. package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
  81. package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
  82. package/3p/pi-plannotator/packages/server/config.ts +8 -0
  83. package/3p/pi-plannotator/packages/server/draft.ts +1 -0
  84. package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
  85. package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
  86. package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
  87. package/3p/pi-plannotator/packages/server/git.ts +141 -0
  88. package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
  89. package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
  90. package/3p/pi-plannotator/packages/server/ide.ts +43 -0
  91. package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
  92. package/3p/pi-plannotator/packages/server/image.ts +66 -0
  93. package/3p/pi-plannotator/packages/server/index.ts +648 -0
  94. package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
  95. package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
  96. package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
  97. package/3p/pi-plannotator/packages/server/jj.ts +86 -0
  98. package/3p/pi-plannotator/packages/server/p4.ts +417 -0
  99. package/3p/pi-plannotator/packages/server/package.json +40 -0
  100. package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
  101. package/3p/pi-plannotator/packages/server/pr.ts +146 -0
  102. package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
  103. package/3p/pi-plannotator/packages/server/project.ts +45 -0
  104. package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
  105. package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
  106. package/3p/pi-plannotator/packages/server/remote.ts +74 -0
  107. package/3p/pi-plannotator/packages/server/repo.ts +80 -0
  108. package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
  109. package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
  110. package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
  111. package/3p/pi-plannotator/packages/server/review.ts +1318 -0
  112. package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
  113. package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
  114. package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
  115. package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
  116. package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
  117. package/3p/pi-plannotator/packages/server/storage.ts +17 -0
  118. package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
  119. package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
  120. package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
  121. package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
  122. package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
  123. package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
  124. package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
  125. package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
  126. package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
  127. package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
  128. package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
  129. package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
  130. package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
  131. package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
  132. package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
  133. package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
  134. package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
  135. package/3p/pi-plannotator/packages/shared/config.ts +262 -0
  136. package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
  137. package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
  138. package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
  139. package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
  140. package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
  141. package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
  142. package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
  143. package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
  144. package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
  145. package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
  146. package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
  147. package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
  148. package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
  149. package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
  150. package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
  151. package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
  152. package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
  153. package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
  154. package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
  155. package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
  156. package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
  157. package/3p/pi-plannotator/packages/shared/package.json +55 -0
  158. package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
  159. package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
  160. package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
  161. package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
  162. package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
  163. package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
  164. package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
  165. package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
  166. package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
  167. package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
  168. package/3p/pi-plannotator/packages/shared/project.ts +71 -0
  169. package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
  170. package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
  171. package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
  172. package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
  173. package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
  174. package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
  175. package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
  176. package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
  177. package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
  178. package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
  179. package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
  180. package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
  181. package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
  182. package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
  183. package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
  184. package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
  185. package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
  186. package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
  187. package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
  188. package/3p/pi-plannotator/packages/shared/types.ts +29 -0
  189. package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
  190. package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
  191. package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
  192. package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
  193. package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
  194. package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
  195. package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
  196. package/3p/pi-subagents/package.json +50 -0
  197. package/3p/pi-subagents/src/agent-manager.ts +413 -0
  198. package/3p/pi-subagents/src/agent-runner.ts +502 -0
  199. package/3p/pi-subagents/src/agent-types.ts +248 -0
  200. package/3p/pi-subagents/src/context.ts +58 -0
  201. package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
  202. package/3p/pi-subagents/src/custom-agents.ts +137 -0
  203. package/3p/pi-subagents/src/default-agents.ts +144 -0
  204. package/3p/pi-subagents/src/env.ts +33 -0
  205. package/3p/pi-subagents/src/group-join.ts +141 -0
  206. package/3p/pi-subagents/src/index.ts +1811 -0
  207. package/3p/pi-subagents/src/invocation-config.ts +40 -0
  208. package/3p/pi-subagents/src/memory.ts +165 -0
  209. package/3p/pi-subagents/src/model-resolver.ts +81 -0
  210. package/3p/pi-subagents/src/output-file.ts +77 -0
  211. package/3p/pi-subagents/src/prompts.ts +85 -0
  212. package/3p/pi-subagents/src/skill-loader.ts +79 -0
  213. package/3p/pi-subagents/src/types.ts +111 -0
  214. package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
  215. package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
  216. package/3p/pi-subagents/src/worktree.ts +162 -0
  217. package/3p/pi-tasks/package.json +52 -0
  218. package/3p/pi-tasks/src/auto-clear.ts +91 -0
  219. package/3p/pi-tasks/src/index.ts +1150 -0
  220. package/3p/pi-tasks/src/process-tracker.ts +140 -0
  221. package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
  222. package/3p/pi-tasks/src/task-store.ts +324 -0
  223. package/3p/pi-tasks/src/tasks-config.ts +27 -0
  224. package/3p/pi-tasks/src/types.ts +40 -0
  225. package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
  226. package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
  227. package/AGENTS.md +28 -0
  228. package/LICENSE +201 -0
  229. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
  230. package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
  231. package/extensions/orchestrator/agents/explore.ts +33 -0
  232. package/extensions/orchestrator/agents/librarian.ts +43 -0
  233. package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
  234. package/extensions/orchestrator/agents/planner.ts +61 -0
  235. package/extensions/orchestrator/agents/registry.ts +156 -0
  236. package/extensions/orchestrator/agents/task.ts +50 -0
  237. package/extensions/orchestrator/agents/tool-routing.ts +84 -0
  238. package/extensions/orchestrator/ast-search.ts +85 -0
  239. package/extensions/orchestrator/cbm.ts +330 -0
  240. package/extensions/orchestrator/command-handlers.test.ts +163 -0
  241. package/extensions/orchestrator/command-handlers.ts +116 -0
  242. package/extensions/orchestrator/commands.test.ts +81 -0
  243. package/extensions/orchestrator/commands.ts +75 -0
  244. package/extensions/orchestrator/config.test.ts +221 -0
  245. package/extensions/orchestrator/config.ts +230 -0
  246. package/extensions/orchestrator/context.test.ts +293 -0
  247. package/extensions/orchestrator/context.ts +206 -0
  248. package/extensions/orchestrator/event-handlers.test.ts +190 -0
  249. package/extensions/orchestrator/event-handlers.ts +1416 -0
  250. package/extensions/orchestrator/exa.ts +104 -0
  251. package/extensions/orchestrator/flant-infra.ts +486 -0
  252. package/extensions/orchestrator/index.ts +80 -0
  253. package/extensions/orchestrator/integration.test.ts +1214 -0
  254. package/extensions/orchestrator/orchestrator.test.ts +252 -0
  255. package/extensions/orchestrator/orchestrator.ts +538 -0
  256. package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
  257. package/extensions/orchestrator/phases/brainstorm.ts +273 -0
  258. package/extensions/orchestrator/phases/implementation.ts +40 -0
  259. package/extensions/orchestrator/phases/machine.test.ts +293 -0
  260. package/extensions/orchestrator/phases/machine.ts +209 -0
  261. package/extensions/orchestrator/phases/planning.ts +255 -0
  262. package/extensions/orchestrator/phases/review.ts +193 -0
  263. package/extensions/orchestrator/plannotator.ts +56 -0
  264. package/extensions/orchestrator/pp-menu.ts +866 -0
  265. package/extensions/orchestrator/state.test.ts +343 -0
  266. package/extensions/orchestrator/state.ts +237 -0
  267. package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
  268. package/extensions/orchestrator/validate-artifacts.ts +272 -0
  269. package/extensions/orchestrator/vendor.d.ts +26 -0
  270. package/package.json +73 -0
  271. package/scripts/postinstall.sh +18 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * Smart markdown file resolution.
3
+ *
4
+ * Resolves a user-provided path to an absolute file path using three strategies:
5
+ * 1. Exact path (absolute or relative to cwd)
6
+ * 2. Case-insensitive relative path search within project root
7
+ * 3. Case-insensitive bare filename search within project root
8
+ *
9
+ * Used by both the CLI (`plannotator annotate`) and the `/api/doc` endpoint.
10
+ */
11
+
12
+ import { homedir } from "os";
13
+ import { isAbsolute, join, resolve, win32 } from "path";
14
+ import { existsSync, readdirSync, type Dirent } from "fs";
15
+
16
+ const MARKDOWN_PATH_REGEX = /\.mdx?$/i;
17
+
18
+ import { CODE_FILE_REGEX as CODE_FILE_BASENAME_REGEX } from "./code-file";
19
+ export { CODE_FILE_REGEX, isCodeFilePath } from "./code-file";
20
+
21
+ const WINDOWS_DRIVE_PATH_PATTERNS = [
22
+ /^\/cygdrive\/([a-zA-Z])\/(.+)$/,
23
+ /^\/([a-zA-Z])\/(.+)$/,
24
+ ];
25
+
26
+ const IGNORED_DIRS = [
27
+ "node_modules/",
28
+ ".git/",
29
+ "dist/",
30
+ "build/",
31
+ ".next/",
32
+ "__pycache__/",
33
+ ".obsidian/",
34
+ ".trash/",
35
+ ];
36
+
37
+ const CODE_IGNORED_DIRS = [
38
+ ...IGNORED_DIRS,
39
+ ".turbo/",
40
+ ".cache/",
41
+ "target/",
42
+ "vendor/",
43
+ "coverage/",
44
+ ".venv/",
45
+ ".pytest_cache/",
46
+ ];
47
+
48
+ export type ResolveResult =
49
+ | { kind: "found"; path: string }
50
+ | { kind: "not_found"; input: string }
51
+ | { kind: "ambiguous"; input: string; matches: string[] }
52
+ | { kind: "unavailable"; input: string };
53
+
54
+ function normalizeSeparators(input: string): string {
55
+ return input.replace(/\\/g, "/");
56
+ }
57
+
58
+ function stripTrailingSlashes(input: string): string {
59
+ return input.replace(/\/+$/, "");
60
+ }
61
+
62
+ export function expandHomePath(input: string, home = homedir()): string {
63
+ if (input === "~") {
64
+ return home;
65
+ }
66
+
67
+ if (input.startsWith("~/") || input.startsWith("~\\")) {
68
+ return join(home, input.slice(2));
69
+ }
70
+
71
+ return input;
72
+ }
73
+
74
+ export function stripWrappingQuotes(input: string): string {
75
+ if (input.length < 2) {
76
+ return input;
77
+ }
78
+
79
+ const first = input[0];
80
+ const last = input[input.length - 1];
81
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
82
+ return input.slice(1, -1);
83
+ }
84
+
85
+ return input;
86
+ }
87
+
88
+ export function normalizeUserPathInput(
89
+ input: string,
90
+ platform = process.platform,
91
+ ): string {
92
+ const trimmedInput = input.trim();
93
+ const unquotedInput = stripWrappingQuotes(trimmedInput);
94
+ const expandedInput = expandHomePath(unquotedInput);
95
+
96
+ if (platform !== "win32") {
97
+ return expandedInput;
98
+ }
99
+
100
+ for (const pattern of WINDOWS_DRIVE_PATH_PATTERNS) {
101
+ const match = expandedInput.match(pattern);
102
+ if (!match) {
103
+ continue;
104
+ }
105
+
106
+ const [, driveLetter, rest] = match;
107
+ return `${driveLetter.toUpperCase()}:/${rest}`;
108
+ }
109
+
110
+ return expandedInput;
111
+ }
112
+
113
+ function isAbsoluteNormalizedUserPath(
114
+ input: string,
115
+ platform = process.platform,
116
+ ): boolean {
117
+ if (hasWindowsDriveLetter(input)) {
118
+ return true;
119
+ }
120
+
121
+ return platform === "win32"
122
+ ? win32.isAbsolute(input)
123
+ : isAbsolute(input);
124
+ }
125
+
126
+ export function isAbsoluteUserPath(
127
+ input: string,
128
+ platform = process.platform,
129
+ ): boolean {
130
+ return isAbsoluteNormalizedUserPath(normalizeUserPathInput(input, platform), platform);
131
+ }
132
+
133
+ export function resolveUserPath(
134
+ input: string,
135
+ baseDir = process.cwd(),
136
+ platform = process.platform,
137
+ ): string {
138
+ const normalizedInput = normalizeUserPathInput(input, platform);
139
+ if (!normalizedInput) {
140
+ return "";
141
+ }
142
+ return isAbsoluteNormalizedUserPath(normalizedInput, platform)
143
+ ? resolveAbsolutePath(normalizedInput, platform)
144
+ : resolve(baseDir, normalizedInput);
145
+ }
146
+
147
+ function normalizeComparablePath(input: string): string {
148
+ return stripTrailingSlashes(normalizeSeparators(resolveUserPath(input)));
149
+ }
150
+
151
+ export function isWithinProjectRoot(candidate: string, projectRoot: string): boolean {
152
+ const normalizedCandidate = normalizeComparablePath(candidate);
153
+ const normalizedProjectRoot = normalizeComparablePath(projectRoot);
154
+ return (
155
+ normalizedCandidate === normalizedProjectRoot ||
156
+ normalizedCandidate.startsWith(`${normalizedProjectRoot}/`)
157
+ );
158
+ }
159
+
160
+ function getLowercaseBasename(input: string): string {
161
+ const normalizedInput = normalizeSeparators(input);
162
+ return normalizedInput.split("/").pop()!.toLowerCase();
163
+ }
164
+
165
+ function getLookupKey(input: string, isBareFilename: boolean): string {
166
+ return isBareFilename ? getLowercaseBasename(input) : input.toLowerCase();
167
+ }
168
+
169
+ function resolveAbsolutePath(
170
+ input: string,
171
+ platform = process.platform,
172
+ ): string {
173
+ // Use win32.resolve for Windows paths regardless of reported platform
174
+ return platform === "win32" || hasWindowsDriveLetter(input)
175
+ ? win32.resolve(input)
176
+ : resolve(input);
177
+ }
178
+
179
+ function isSearchableMarkdownPath(input: string): boolean {
180
+ return MARKDOWN_PATH_REGEX.test(input.trim());
181
+ }
182
+
183
+ /** Check if a path looks like a Windows absolute path (e.g. C:\ or C:/) */
184
+ function hasWindowsDriveLetter(input: string): boolean {
185
+ return /^[a-zA-Z]:[/\\]/.test(input);
186
+ }
187
+
188
+ /** Cross-platform file existence check using Node fs (more reliable than Bun.file in compiled exes) */
189
+ function fileExists(filePath: string): boolean {
190
+ try {
191
+ return existsSync(filePath);
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ /** Recursively walk a directory collecting files matching `fileMatcher`, skipping ignored dirs. */
198
+ function walkFiles(
199
+ dir: string,
200
+ root: string,
201
+ results: string[],
202
+ ignoredDirs: string[],
203
+ fileMatcher: (name: string) => boolean,
204
+ ): void {
205
+ const entries = readdirSync(dir, { withFileTypes: true }) as Dirent[];
206
+ for (const entry of entries) {
207
+ if (entry.isDirectory()) {
208
+ if (ignoredDirs.some((d) => d === entry.name + "/")) continue;
209
+ try {
210
+ walkFiles(join(dir, entry.name), root, results, ignoredDirs, fileMatcher);
211
+ } catch {
212
+ /* skip dirs we can't read */
213
+ }
214
+ } else if (entry.isFile() && fileMatcher(entry.name)) {
215
+ const relative = join(dir, entry.name)
216
+ .slice(root.length + 1)
217
+ .replace(/\\/g, "/");
218
+ results.push(relative);
219
+ }
220
+ }
221
+ }
222
+
223
+ function walkMarkdownFiles(dir: string, root: string, results: string[], ignoredDirs: string[]): void {
224
+ try {
225
+ walkFiles(dir, root, results, ignoredDirs, (name) => /\.mdx?$/i.test(name));
226
+ } catch {
227
+ /* fail soft for markdown — preserves existing behavior */
228
+ }
229
+ }
230
+
231
+ // --- Code-file resolution (async, cached) ---
232
+
233
+ const FILE_LIST_CACHE_TTL_MS = 30_000;
234
+ const fileListCache = new Map<
235
+ string,
236
+ { promise: Promise<string[] | null>; startedAt: number }
237
+ >();
238
+
239
+ function fileListCacheKey(projectRoot: string, kind: string): string {
240
+ return `${projectRoot}|${kind}`;
241
+ }
242
+
243
+ function startCodeWalk(projectRoot: string): Promise<string[] | null> {
244
+ return Promise.resolve().then(() => {
245
+ try {
246
+ const results: string[] = [];
247
+ walkFiles(projectRoot, projectRoot, results, CODE_IGNORED_DIRS, (name) =>
248
+ CODE_FILE_BASENAME_REGEX.test(name),
249
+ );
250
+ return results;
251
+ } catch {
252
+ return null;
253
+ }
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Trigger (or return the in-flight) walk of `projectRoot` for code files.
259
+ * Cached for `FILE_LIST_CACHE_TTL_MS`. Storing a Promise (not a value) makes
260
+ * concurrent callers piggyback on the same walk — first arrival wins.
261
+ *
262
+ * Returns `null` (wrapped in Promise) when the walk fails (perms, etc).
263
+ */
264
+ export function warmFileListCache(
265
+ projectRoot: string,
266
+ kind: "code",
267
+ ): Promise<string[] | null> {
268
+ const key = fileListCacheKey(projectRoot, kind);
269
+ const entry = fileListCache.get(key);
270
+ if (entry && Date.now() - entry.startedAt < FILE_LIST_CACHE_TTL_MS) {
271
+ return entry.promise;
272
+ }
273
+ const promise = startCodeWalk(projectRoot);
274
+ fileListCache.set(key, { promise, startedAt: Date.now() });
275
+ return promise;
276
+ }
277
+
278
+ /**
279
+ * Resolve a code-file path within a project root.
280
+ *
281
+ * Strategies:
282
+ * 1. Absolute path → use as-is.
283
+ * 2. Exact relative from project root.
284
+ * 3. If `baseDir` provided, literal `<baseDir>/<input>` existence check —
285
+ * lets out-of-tree linked docs resolve their own relative references
286
+ * (e.g. `../script.ts` in `~/notes/foo.md` finds `~/script.ts`).
287
+ * 4. Case-insensitive suffix match against the cached file list:
288
+ * - bare basename input → match any file with that basename;
289
+ * - input with `/` → match files whose path equals or ends with `/<input>`
290
+ * on a segment boundary (so `editor/App.tsx` matches `packages/editor/App.tsx`
291
+ * but not `myeditor/App.tsx`).
292
+ *
293
+ * `..` segments in the input are honored: only `./` is stripped before suffix
294
+ * matching. `../foo.ts` without a `baseDir` correctly falls through to
295
+ * not_found rather than fabricating a match against `foo.ts` somewhere in cwd.
296
+ */
297
+ export async function resolveCodeFile(
298
+ input: string,
299
+ projectRoot: string,
300
+ baseDir?: string,
301
+ ): Promise<ResolveResult> {
302
+ const originalInput = input.trim();
303
+ const unquotedInput = stripWrappingQuotes(originalInput);
304
+ const normalizedInput = normalizeUserPathInput(unquotedInput);
305
+ const searchInput = normalizeSeparators(normalizedInput);
306
+
307
+ if (!searchInput) {
308
+ return { kind: "not_found", input: originalInput };
309
+ }
310
+
311
+ if (isAbsoluteNormalizedUserPath(normalizedInput)) {
312
+ const absolutePath = resolveAbsolutePath(normalizedInput);
313
+ if (fileExists(absolutePath)) {
314
+ return { kind: "found", path: absolutePath };
315
+ }
316
+ return { kind: "not_found", input: originalInput };
317
+ }
318
+
319
+ const fromRoot = resolve(projectRoot, searchInput);
320
+ if (isWithinProjectRoot(fromRoot, projectRoot) && fileExists(fromRoot)) {
321
+ return { kind: "found", path: fromRoot };
322
+ }
323
+
324
+ if (baseDir) {
325
+ const fromBase = resolve(baseDir, searchInput);
326
+ if (fileExists(fromBase)) {
327
+ return { kind: "found", path: fromBase };
328
+ }
329
+ }
330
+
331
+ const fileList = await warmFileListCache(projectRoot, "code");
332
+ if (fileList === null) {
333
+ return { kind: "unavailable", input: originalInput };
334
+ }
335
+
336
+ // Strip leading `./` so suffix matching works on inputs like
337
+ // `./editor/App.tsx` — file list entries never carry that segment.
338
+ // `../` is intentionally NOT stripped: `..` is meaningful (escape parent),
339
+ // not noise. If we can't honor it via baseDir, the input has no
340
+ // suffix-match equivalent in the in-tree file list.
341
+ const cleanedInput = searchInput.replace(/^(?:\.\/)+/, "");
342
+ if (!cleanedInput || cleanedInput.startsWith("../")) {
343
+ return { kind: "not_found", input: originalInput };
344
+ }
345
+ const target = cleanedInput.toLowerCase();
346
+ const isBareFilename = !cleanedInput.includes("/");
347
+ const matches: string[] = [];
348
+
349
+ for (const f of fileList) {
350
+ const fl = f.toLowerCase();
351
+ if (isBareFilename) {
352
+ const base = fl.split("/").pop();
353
+ if (base === target) matches.push(resolve(projectRoot, f));
354
+ } else {
355
+ if (fl === target || fl.endsWith("/" + target)) {
356
+ matches.push(resolve(projectRoot, f));
357
+ }
358
+ }
359
+ }
360
+
361
+ if (matches.length === 1) {
362
+ return { kind: "found", path: matches[0] };
363
+ }
364
+ if (matches.length > 1) {
365
+ return { kind: "ambiguous", input: originalInput, matches };
366
+ }
367
+ return { kind: "not_found", input: originalInput };
368
+ }
369
+
370
+ /**
371
+ * Resolve a markdown file path within a project root.
372
+ *
373
+ * @param input - User-provided path (absolute, relative, or bare filename)
374
+ * @param projectRoot - Project root directory to search within
375
+ */
376
+ function resolveMarkdownFileCore(
377
+ input: string,
378
+ projectRoot: string,
379
+ ): ResolveResult {
380
+ const normalizedInput = normalizeUserPathInput(input);
381
+ const searchInput = normalizeSeparators(normalizedInput);
382
+ const isBareFilename = !searchInput.includes("/");
383
+ const targetLookupKey = getLookupKey(searchInput, isBareFilename);
384
+
385
+ // Restrict to markdown files
386
+ if (!isSearchableMarkdownPath(normalizedInput)) {
387
+ return { kind: "not_found", input };
388
+ }
389
+
390
+ // 1. Absolute path — use as-is (no project root restriction;
391
+ // the user explicitly typed the full path)
392
+ if (isAbsoluteNormalizedUserPath(normalizedInput)) {
393
+ const absolutePath = resolveAbsolutePath(normalizedInput);
394
+ if (fileExists(absolutePath)) {
395
+ return { kind: "found", path: absolutePath };
396
+ }
397
+ return { kind: "not_found", input };
398
+ }
399
+
400
+ // 2. Exact relative path from project root
401
+ const fromRoot = resolve(projectRoot, searchInput);
402
+ if (isWithinProjectRoot(fromRoot, projectRoot) && fileExists(fromRoot)) {
403
+ return { kind: "found", path: fromRoot };
404
+ }
405
+
406
+ // 3. Case-insensitive search (only scan markdown files)
407
+ const allFiles: string[] = [];
408
+ walkMarkdownFiles(projectRoot, projectRoot, allFiles, IGNORED_DIRS);
409
+ const matches: string[] = [];
410
+
411
+ for (const match of allFiles) {
412
+ const normalizedMatch = normalizeSeparators(match);
413
+ const matchLookupKey = getLookupKey(normalizedMatch, isBareFilename);
414
+
415
+ if (matchLookupKey === targetLookupKey) {
416
+ const full = resolve(projectRoot, normalizedMatch);
417
+ if (isWithinProjectRoot(full, projectRoot)) {
418
+ matches.push(full);
419
+ }
420
+ }
421
+ }
422
+
423
+ if (matches.length === 1) {
424
+ return { kind: "found", path: matches[0] };
425
+ }
426
+ if (matches.length > 1) {
427
+ const projectRootPrefix = `${normalizeComparablePath(projectRoot)}/`;
428
+ const relative = matches.map((match) =>
429
+ normalizeComparablePath(match).replace(projectRootPrefix, ""),
430
+ );
431
+ return { kind: "ambiguous", input, matches: relative };
432
+ }
433
+
434
+ return { kind: "not_found", input };
435
+ }
436
+
437
+ /**
438
+ * Resolve a markdown file path within a project root.
439
+ *
440
+ * @param input - User-provided path (absolute, relative, or bare filename)
441
+ * @param projectRoot - Project root directory to search within
442
+ */
443
+ export function resolveMarkdownFile(
444
+ input: string,
445
+ projectRoot: string,
446
+ ): ResolveResult {
447
+ const originalInput = input.trim();
448
+ const unquotedInput = stripWrappingQuotes(originalInput);
449
+
450
+ const primary = resolveMarkdownFileCore(unquotedInput, projectRoot);
451
+ if (primary.kind === "found") {
452
+ return primary;
453
+ }
454
+ if (primary.kind === "ambiguous") {
455
+ return { ...primary, input: originalInput };
456
+ }
457
+
458
+ if (!unquotedInput.startsWith("@")) {
459
+ return { kind: "not_found", input: originalInput };
460
+ }
461
+
462
+ const normalizedInput = unquotedInput.replace(/^@+/, "");
463
+ if (!normalizedInput) {
464
+ return { kind: "not_found", input: originalInput };
465
+ }
466
+
467
+ const fallback = resolveMarkdownFileCore(normalizedInput, projectRoot);
468
+ if (fallback.kind === "found") {
469
+ return fallback;
470
+ }
471
+ if (fallback.kind === "ambiguous") {
472
+ return { ...fallback, input: originalInput };
473
+ }
474
+
475
+ return { kind: "not_found", input: originalInput };
476
+ }
477
+
478
+ /**
479
+ * Check if a directory contains at least one file matching the given extensions.
480
+ * Used to validate folder annotation targets.
481
+ *
482
+ * @param dirPath - Directory to search
483
+ * @param excludedDirs - Directory names to skip (with trailing slash, e.g. "node_modules/")
484
+ * @param extensions - Regex to match file extensions (default: markdown only)
485
+ */
486
+ export function hasMarkdownFiles(
487
+ dirPath: string,
488
+ excludedDirs: string[] = IGNORED_DIRS,
489
+ extensions: RegExp = /\.mdx?$/i,
490
+ ): boolean {
491
+ function walk(dir: string): boolean {
492
+ let entries;
493
+ try {
494
+ entries = readdirSync(dir, { withFileTypes: true });
495
+ } catch {
496
+ return false;
497
+ }
498
+ for (const entry of entries) {
499
+ if (entry.isDirectory()) {
500
+ if (excludedDirs.some((d) => d === entry.name + "/")) continue;
501
+ if (walk(join(dir, entry.name))) return true;
502
+ } else if (entry.isFile() && extensions.test(entry.name)) {
503
+ return true;
504
+ }
505
+ }
506
+ return false;
507
+ }
508
+ return walk(dirPath);
509
+ }
@@ -0,0 +1,64 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { parseReviewArgs } from "./review-args";
3
+
4
+ describe("parseReviewArgs", () => {
5
+ test("defaults to auto VCS and local PR checkout", () => {
6
+ expect(parseReviewArgs("")).toEqual({
7
+ prUrl: undefined,
8
+ vcsType: undefined,
9
+ useLocal: true,
10
+ });
11
+ });
12
+
13
+ test("parses --git without a PR URL", () => {
14
+ expect(parseReviewArgs("--git")).toEqual({
15
+ prUrl: undefined,
16
+ vcsType: "git",
17
+ useLocal: true,
18
+ });
19
+ });
20
+
21
+ test("parses PR URLs before or after --git", () => {
22
+ expect(parseReviewArgs("--git https://github.com/acme/repo/pull/12")).toEqual({
23
+ prUrl: "https://github.com/acme/repo/pull/12",
24
+ vcsType: "git",
25
+ useLocal: true,
26
+ });
27
+ expect(parseReviewArgs("https://github.com/acme/repo/pull/12 --git")).toEqual({
28
+ prUrl: "https://github.com/acme/repo/pull/12",
29
+ vcsType: "git",
30
+ useLocal: true,
31
+ });
32
+ });
33
+
34
+ test("preserves --no-local for PR review mode", () => {
35
+ expect(parseReviewArgs("--no-local https://github.com/acme/repo/pull/12")).toEqual({
36
+ prUrl: "https://github.com/acme/repo/pull/12",
37
+ vcsType: undefined,
38
+ useLocal: false,
39
+ });
40
+ });
41
+
42
+ test("accepts argv arrays from the compiled CLI", () => {
43
+ expect(parseReviewArgs(["--git", "--no-local", "https://github.com/acme/repo/pull/12"])).toEqual({
44
+ prUrl: "https://github.com/acme/repo/pull/12",
45
+ vcsType: "git",
46
+ useLocal: false,
47
+ });
48
+ });
49
+
50
+ test("strips wrapping quotes from string and argv inputs", () => {
51
+ expect(parseReviewArgs(`--git "https://github.com/acme/repo/pull/12"`).prUrl)
52
+ .toBe("https://github.com/acme/repo/pull/12");
53
+ expect(parseReviewArgs(["--git", "\"https://github.com/acme/repo/pull/12\""]).prUrl)
54
+ .toBe("https://github.com/acme/repo/pull/12");
55
+ });
56
+
57
+ test("keeps non-url positional input as local review mode", () => {
58
+ expect(parseReviewArgs("--git not-a-url")).toEqual({
59
+ prUrl: undefined,
60
+ vcsType: "git",
61
+ useLocal: true,
62
+ });
63
+ });
64
+ });
@@ -0,0 +1,85 @@
1
+ import type { VcsSelection } from "./vcs-core";
2
+ import { stripWrappingQuotes } from "./resolve-file";
3
+
4
+ export interface ParsedReviewArgs {
5
+ prUrl?: string;
6
+ vcsType?: VcsSelection;
7
+ useLocal: boolean;
8
+ }
9
+
10
+ export function parseReviewArgs(input: string | string[]): ParsedReviewArgs {
11
+ const tokens = Array.isArray(input)
12
+ ? input.map((token) => stripWrappingQuotes(token.trim())).filter(Boolean)
13
+ : tokenizeReviewArgs(input ?? "");
14
+
15
+ let vcsType: VcsSelection | undefined;
16
+ let useLocal = true;
17
+ const positional: string[] = [];
18
+
19
+ for (const token of tokens) {
20
+ switch (token) {
21
+ case "--git":
22
+ vcsType = "git";
23
+ break;
24
+ case "--local":
25
+ useLocal = true;
26
+ break;
27
+ case "--no-local":
28
+ useLocal = false;
29
+ break;
30
+ default:
31
+ positional.push(token);
32
+ break;
33
+ }
34
+ }
35
+
36
+ const target = positional[0];
37
+ return {
38
+ prUrl: target && isReviewUrl(target) ? target : undefined,
39
+ vcsType,
40
+ useLocal,
41
+ };
42
+ }
43
+
44
+ function isReviewUrl(value: string): boolean {
45
+ return value.startsWith("http://") || value.startsWith("https://");
46
+ }
47
+
48
+ function tokenizeReviewArgs(input: string): string[] {
49
+ const raw = input.trim();
50
+ if (!raw) return [];
51
+
52
+ const tokens: string[] = [];
53
+ let current = "";
54
+ let quote: "'" | "\"" | undefined;
55
+
56
+ for (let i = 0; i < raw.length; i++) {
57
+ const char = raw[i];
58
+ if (quote) {
59
+ if (char === quote) {
60
+ quote = undefined;
61
+ } else {
62
+ current += char;
63
+ }
64
+ continue;
65
+ }
66
+
67
+ if (char === "'" || char === "\"") {
68
+ quote = char;
69
+ continue;
70
+ }
71
+
72
+ if (/\s/.test(char)) {
73
+ if (current) {
74
+ tokens.push(current);
75
+ current = "";
76
+ }
77
+ continue;
78
+ }
79
+
80
+ current += char;
81
+ }
82
+
83
+ if (current) tokens.push(current);
84
+ return tokens.map((token) => stripWrappingQuotes(token.trim())).filter(Boolean);
85
+ }