@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,243 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+
4
+ // --- Types ---
5
+
6
+ export interface ObsidianConfig {
7
+ vaultPath: string;
8
+ folder: string;
9
+ plan: string;
10
+ filenameFormat?: string; // Custom format string, e.g. '{YYYY}-{MM}-{DD} - {title}'
11
+ filenameSeparator?: "space" | "dash" | "underscore"; // Replace spaces in filename
12
+ }
13
+
14
+ export interface BearConfig {
15
+ plan: string;
16
+ customTags?: string;
17
+ tagPosition?: "prepend" | "append";
18
+ }
19
+
20
+ export interface OctarineConfig {
21
+ plan: string;
22
+ workspace: string;
23
+ folder: string;
24
+ }
25
+
26
+ export interface IntegrationResult {
27
+ success: boolean;
28
+ error?: string;
29
+ path?: string;
30
+ }
31
+
32
+ /**
33
+ * Detect Obsidian vaults by reading Obsidian's config file
34
+ * Returns array of vault paths found on the system
35
+ */
36
+ export function detectObsidianVaults(): string[] {
37
+ try {
38
+ const home = process.env.HOME || process.env.USERPROFILE || "";
39
+ let configPath: string;
40
+
41
+ // Platform-specific config locations
42
+ if (process.platform === "darwin") {
43
+ configPath = join(
44
+ home,
45
+ "Library/Application Support/obsidian/obsidian.json",
46
+ );
47
+ } else if (process.platform === "win32") {
48
+ const appData = process.env.APPDATA || join(home, "AppData/Roaming");
49
+ configPath = join(appData, "obsidian/obsidian.json");
50
+ } else {
51
+ // Linux
52
+ configPath = join(home, ".config/obsidian/obsidian.json");
53
+ }
54
+
55
+ if (!existsSync(configPath)) {
56
+ return [];
57
+ }
58
+
59
+ const configContent = readFileSync(configPath, "utf-8");
60
+ const config = JSON.parse(configContent);
61
+
62
+ if (!config.vaults || typeof config.vaults !== "object") {
63
+ return [];
64
+ }
65
+
66
+ // Extract vault paths, filter to ones that exist
67
+ const vaults: string[] = [];
68
+ for (const vaultId of Object.keys(config.vaults)) {
69
+ const vault = config.vaults[vaultId];
70
+ if (vault.path && existsSync(vault.path)) {
71
+ vaults.push(vault.path);
72
+ }
73
+ }
74
+
75
+ return vaults;
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
81
+ // --- Frontmatter and Filename Generation ---
82
+
83
+ /**
84
+ * Generate frontmatter for the note
85
+ */
86
+ export function generateFrontmatter(tags: string[]): string {
87
+ const now = new Date().toISOString();
88
+ const tagList = tags.map((t) => t.toLowerCase()).join(", ");
89
+ return `---
90
+ created: ${now}
91
+ source: plannotator
92
+ tags: [${tagList}]
93
+ ---`;
94
+ }
95
+
96
+ /**
97
+ * Extract title from markdown (first H1 heading)
98
+ */
99
+ export function extractTitle(markdown: string): string {
100
+ const h1Match = markdown.match(
101
+ /^#\s+(?:Implementation\s+Plan:|Plan:)?\s*(.+)$/im,
102
+ );
103
+ if (h1Match) {
104
+ // Clean up the title for use as filename
105
+ return h1Match[1]
106
+ .trim()
107
+ .replace(/[<>:"/\\|?*(){}\[\]#~`]/g, "") // Remove invalid/problematic filename chars
108
+ .replace(/\s+/g, " ") // Normalize whitespace
109
+ .trim() // Re-trim after stripping
110
+ .slice(0, 50); // Limit length
111
+ }
112
+ return "Plan";
113
+ }
114
+
115
+ /** Default filename format matching original behavior */
116
+ export const DEFAULT_FILENAME_FORMAT =
117
+ "{title} - {Mon} {D}, {YYYY} {h}-{mm}{ampm}";
118
+
119
+ /**
120
+ * Generate filename from a format string with variable substitution.
121
+ *
122
+ * Supported variables:
123
+ * {title} - Plan title from first H1 heading
124
+ * {YYYY} - 4-digit year
125
+ * {MM} - 2-digit month (01-12)
126
+ * {DD} - 2-digit day (01-31)
127
+ * {Mon} - Abbreviated month name (Jan, Feb, ...)
128
+ * {D} - Day without leading zero
129
+ * {HH} - 2-digit hour, 24h (00-23)
130
+ * {h} - Hour without leading zero, 12h
131
+ * {hh} - 2-digit hour, 12h (01-12)
132
+ * {mm} - 2-digit minutes (00-59)
133
+ * {ss} - 2-digit seconds (00-59)
134
+ * {ampm} - am/pm
135
+ *
136
+ * Default format: '{title} - {Mon} {D}, {YYYY} {h}-{mm}{ampm}'
137
+ * Example output: 'User Authentication - Jan 2, 2026 2-30pm.md'
138
+ */
139
+ export function generateFilename(
140
+ markdown: string,
141
+ format?: string,
142
+ separator?: "space" | "dash" | "underscore",
143
+ ): string {
144
+ const title = extractTitle(markdown);
145
+ const now = new Date();
146
+
147
+ const months = [
148
+ "Jan",
149
+ "Feb",
150
+ "Mar",
151
+ "Apr",
152
+ "May",
153
+ "Jun",
154
+ "Jul",
155
+ "Aug",
156
+ "Sep",
157
+ "Oct",
158
+ "Nov",
159
+ "Dec",
160
+ ];
161
+
162
+ const hour24 = now.getHours();
163
+ const hour12 = hour24 % 12 || 12;
164
+ const ampm = hour24 >= 12 ? "pm" : "am";
165
+
166
+ const vars: Record<string, string> = {
167
+ title,
168
+ YYYY: String(now.getFullYear()),
169
+ MM: String(now.getMonth() + 1).padStart(2, "0"),
170
+ DD: String(now.getDate()).padStart(2, "0"),
171
+ Mon: months[now.getMonth()],
172
+ D: String(now.getDate()),
173
+ HH: String(hour24).padStart(2, "0"),
174
+ h: String(hour12),
175
+ hh: String(hour12).padStart(2, "0"),
176
+ mm: String(now.getMinutes()).padStart(2, "0"),
177
+ ss: String(now.getSeconds()).padStart(2, "0"),
178
+ ampm,
179
+ };
180
+
181
+ const template = format?.trim() || DEFAULT_FILENAME_FORMAT;
182
+ const result = template.replace(
183
+ /\{(\w+)\}/g,
184
+ (match, key) => vars[key] ?? match,
185
+ );
186
+
187
+ // Sanitize: remove characters invalid in filenames
188
+ let sanitized = result
189
+ .replace(/[<>:"/\\|?*]/g, "")
190
+ .replace(/\s+/g, " ")
191
+ .trim();
192
+
193
+ // Apply separator preference (replace spaces with dash or underscore)
194
+ if (separator === "dash") {
195
+ sanitized = sanitized.replace(/ /g, "-");
196
+ } else if (separator === "underscore") {
197
+ sanitized = sanitized.replace(/ /g, "_");
198
+ }
199
+
200
+ return sanitized.endsWith(".md") ? sanitized : `${sanitized}.md`;
201
+ }
202
+
203
+ // --- Bear Integration ---
204
+
205
+ export function stripH1(plan: string): string {
206
+ return plan.replace(/^#\s+.+\n?/m, "").trimStart();
207
+ }
208
+
209
+ export function buildHashtags(
210
+ customTags: string | undefined,
211
+ autoTags: string[],
212
+ ): string {
213
+ if (customTags?.trim()) {
214
+ return customTags
215
+ .split(",")
216
+ .map((t) => `#${t.trim()}`)
217
+ .filter((t) => t !== "#")
218
+ .join(" ");
219
+ }
220
+ return autoTags.map((t) => `#${t}`).join(" ");
221
+ }
222
+
223
+ export function buildBearContent(
224
+ body: string,
225
+ hashtags: string,
226
+ tagPosition: "prepend" | "append",
227
+ ): string {
228
+ return tagPosition === "prepend"
229
+ ? `${hashtags}\n\n${body}`
230
+ : `${body}\n\n${hashtags}`;
231
+ }
232
+
233
+ // --- Octarine Integration ---
234
+
235
+ /**
236
+ * Generate YAML frontmatter for an Octarine note.
237
+ * Uses Octarine's property format (list-style tags, Status, Author, Last Edited).
238
+ */
239
+ export function generateOctarineFrontmatter(tags: string[]): string {
240
+ const now = new Date().toISOString().slice(0, 16); // YYYY-MM-DDTHH:MM
241
+ const tagLines = tags.map((t) => ` - ${t.toLowerCase()}`).join("\n");
242
+ return `---\ntags:\n${tagLines}\nStatus: Draft\nAuthor: plannotator\nLast Edited: ${now}\n---`;
243
+ }
@@ -0,0 +1,236 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getJjDiffArgs,
4
+ getJjEvoLogEntries,
5
+ jjLineBaseRevset,
6
+ parseJjBookmarkList,
7
+ parseJjRemoteBookmarkList,
8
+ type ReviewJjRuntime,
9
+ runJjDiff,
10
+ selectDefaultJjCompareTarget,
11
+ } from "./jj-core";
12
+
13
+ describe("jj diff args", () => {
14
+ test("builds git-format diff args for each jj mode", () => {
15
+ expect(getJjDiffArgs("jj-current", "trunk()")).toEqual({
16
+ args: ["diff", "--git", "-r", "@"],
17
+ label: "Current change",
18
+ });
19
+
20
+ expect(getJjDiffArgs("jj-last", "trunk()")).toEqual({
21
+ args: ["diff", "--git", "-r", "@-"],
22
+ label: "Last change",
23
+ });
24
+
25
+ expect(getJjDiffArgs("jj-line", "trunk()")).toEqual({
26
+ args: ["diff", "--git", "--from", "heads(::@ & ::(trunk()))", "--to", "@"],
27
+ label: "Line of work vs trunk()",
28
+ });
29
+
30
+ expect(getJjDiffArgs("jj-all", "trunk()")).toEqual({
31
+ args: ["diff", "--git", "--from", "root()", "--to", "@"],
32
+ label: "All files",
33
+ });
34
+ });
35
+
36
+ test("preserves hide-whitespace in every jj diff mode", () => {
37
+ expect(getJjDiffArgs("jj-current", "trunk()", { hideWhitespace: true })?.args)
38
+ .toEqual(["diff", "--git", "-w", "-r", "@"]);
39
+ expect(getJjDiffArgs("jj-last", "trunk()", { hideWhitespace: true })?.args)
40
+ .toEqual(["diff", "--git", "-w", "-r", "@-"]);
41
+ expect(getJjDiffArgs("jj-line", "trunk()", { hideWhitespace: true })?.args)
42
+ .toEqual(["diff", "--git", "-w", "--from", "heads(::@ & ::(trunk()))", "--to", "@"]);
43
+ expect(getJjDiffArgs("jj-all", "trunk()", { hideWhitespace: true })?.args)
44
+ .toEqual(["diff", "--git", "-w", "--from", "root()", "--to", "@"]);
45
+ });
46
+
47
+ test("drops hunk-less file chunks after hide-whitespace filtering", async () => {
48
+ const runtimeForPatch = (stdout: string): ReviewJjRuntime => ({
49
+ async runJj() {
50
+ return {
51
+ stdout,
52
+ stderr: "",
53
+ exitCode: 0,
54
+ };
55
+ },
56
+ });
57
+
58
+ const hunklessChunk = [
59
+ "diff --git a/spacey.ts b/spacey.ts",
60
+ "index 1111111..2222222 100644",
61
+ "--- a/spacey.ts",
62
+ "+++ b/spacey.ts",
63
+ "",
64
+ ].join("\n");
65
+ const realChunk = [
66
+ "diff --git a/real.ts b/real.ts",
67
+ "index 3333333..4444444 100644",
68
+ "--- a/real.ts",
69
+ "+++ b/real.ts",
70
+ "@@ -1 +1 @@",
71
+ "-old",
72
+ "+new",
73
+ "",
74
+ ].join("\n");
75
+
76
+ const result = await runJjDiff(runtimeForPatch(hunklessChunk + realChunk), "jj-current", "trunk()", undefined, { hideWhitespace: true });
77
+
78
+ expect(result.patch).not.toContain("spacey.ts");
79
+ expect(result.patch).toContain("real.ts");
80
+ expect(result.patch).toContain("@@ -1 +1 @@");
81
+
82
+ const emptyResult = await runJjDiff(runtimeForPatch(hunklessChunk), "jj-current", "trunk()", undefined, { hideWhitespace: true });
83
+ expect(emptyResult.patch).toBe("");
84
+ });
85
+ });
86
+
87
+ describe("jj compare targets", () => {
88
+ test("resolves default target from jj trunk remote bookmarks", async () => {
89
+ const calls: string[][] = [];
90
+ const runtime: ReviewJjRuntime = {
91
+ async runJj(args) {
92
+ calls.push(args);
93
+ return { stdout: '[{"name":"main"},{"name":"main","remote":"origin"}]\n', stderr: "", exitCode: 0 };
94
+ },
95
+ };
96
+
97
+ await expect(selectDefaultJjCompareTarget(runtime, "/repo"))
98
+ .resolves.toBe("main@origin");
99
+ expect(calls).toEqual([[
100
+ "log",
101
+ "--no-graph",
102
+ "-r",
103
+ "trunk()",
104
+ "-T",
105
+ "json(bookmarks)",
106
+ ]]);
107
+ });
108
+
109
+ test("falls back to local bookmark then trunk revset", async () => {
110
+ const runtimeFor = (stdout: string): ReviewJjRuntime => ({
111
+ async runJj() {
112
+ return { stdout, stderr: "", exitCode: 0 };
113
+ },
114
+ });
115
+
116
+ await expect(selectDefaultJjCompareTarget(runtimeFor('[{"name":"develop"}]\n')))
117
+ .resolves.toBe("develop");
118
+ await expect(selectDefaultJjCompareTarget(runtimeFor('[]\n')))
119
+ .resolves.toBe("trunk()");
120
+ });
121
+
122
+ test("treats bookmarks and revsets correctly in line-of-work revsets", () => {
123
+ expect(jjLineBaseRevset("main")).toBe('heads(::@ & ::(bookmarks(exact:"main")))');
124
+ expect(jjLineBaseRevset("main@origin")).toBe('heads(::@ & ::(remote_bookmarks(exact:"main", exact:"origin")))');
125
+ expect(jjLineBaseRevset("trunk()")).toBe("heads(::@ & ::(trunk()))");
126
+ });
127
+ });
128
+
129
+ describe("jj evolog", () => {
130
+ test("builds evolog diff args with explicit base", () => {
131
+ expect(getJjDiffArgs("jj-evolog", "abc123456789")).toEqual({
132
+ args: ["diff", "--git", "--from", "abc123456789", "--to", "@"],
133
+ label: "Evolution diff from abc12345",
134
+ });
135
+ });
136
+
137
+ test("builds evolog diff args with whitespace flag", () => {
138
+ expect(getJjDiffArgs("jj-evolog", "abc123456789", { hideWhitespace: true })?.args)
139
+ .toEqual(["diff", "--git", "-w", "--from", "abc123456789", "--to", "@"]);
140
+ });
141
+
142
+ test("parses evolog output correctly (commit.* template fields)", async () => {
143
+ const runtime: ReviewJjRuntime = {
144
+ async runJj() {
145
+ return {
146
+ stdout: [
147
+ "abc123456789\tAdd login form\t2 minutes ago",
148
+ "def456789012\tAdd login form\t10 minutes ago",
149
+ "ghi789012345\tAdd login form\t1 hour ago",
150
+ "",
151
+ ].join("\n"),
152
+ stderr: "",
153
+ exitCode: 0,
154
+ };
155
+ },
156
+ };
157
+ const entries = await getJjEvoLogEntries(runtime);
158
+ expect(entries).toHaveLength(3);
159
+ expect(entries[0]).toEqual({ commitId: "abc123456789", description: "Add login form", age: "2 minutes ago" });
160
+ expect(entries[1]).toEqual({ commitId: "def456789012", description: "Add login form", age: "10 minutes ago" });
161
+ expect(entries[2]).toEqual({ commitId: "ghi789012345", description: "Add login form", age: "1 hour ago" });
162
+ });
163
+
164
+ test("returns empty array when evolog exits non-zero", async () => {
165
+ const runtime: ReviewJjRuntime = {
166
+ async runJj() {
167
+ return { stdout: "", stderr: "error: no such revision", exitCode: 1 };
168
+ },
169
+ };
170
+ const entries = await getJjEvoLogEntries(runtime);
171
+ expect(entries).toHaveLength(0);
172
+ });
173
+
174
+ test("defaults to second evolog entry when no base given", async () => {
175
+ let callCount = 0;
176
+ const calls: string[][] = [];
177
+ const runtime: ReviewJjRuntime = {
178
+ async runJj(args) {
179
+ calls.push(args);
180
+ callCount++;
181
+ if (args[0] === "evolog") {
182
+ return {
183
+ stdout: [
184
+ "abc123456789\tFix bug\t1 minute ago",
185
+ "def456789012\tFix bug\t5 minutes ago",
186
+ "",
187
+ ].join("\n"),
188
+ stderr: "",
189
+ exitCode: 0,
190
+ };
191
+ }
192
+ return { stdout: "diff --git a/foo.ts b/foo.ts\n", stderr: "", exitCode: 0 };
193
+ },
194
+ };
195
+ const result = await runJjDiff(runtime, "jj-evolog", "");
196
+ expect(result.label).toBe("Evolution diff from def45678");
197
+ // evolog call + diff call
198
+ expect(callCount).toBe(2);
199
+ const diffCall = calls.find((c) => c[0] === "diff");
200
+ expect(diffCall).toEqual(["diff", "--git", "--from", "def456789012", "--to", "@"]);
201
+ });
202
+
203
+ test("returns error when evolog has no previous entry", async () => {
204
+ const runtime: ReviewJjRuntime = {
205
+ async runJj(args) {
206
+ if (args[0] === "evolog") {
207
+ return { stdout: "abc123456789\tInitial commit\t1 minute ago\n", stderr: "", exitCode: 0 };
208
+ }
209
+ return { stdout: "", stderr: "", exitCode: 0 };
210
+ },
211
+ };
212
+ const result = await runJjDiff(runtime, "jj-evolog", "");
213
+ expect(result.error).toBe("No previous evolution found");
214
+ expect(result.patch).toBe("");
215
+ });
216
+ });
217
+
218
+ describe("jj bookmark parsing", () => {
219
+ test("parses escaped newline separators from jj bookmark templates", () => {
220
+ expect(parseJjBookmarkList('"dev"\\n"main"\\n')).toEqual(["dev", "main"]);
221
+ });
222
+
223
+ test("parses escaped tab and newline separators from jj remote bookmark templates", () => {
224
+ expect(parseJjRemoteBookmarkList('"main"\\t"git"\\n"release"\\t"origin"\\n')).toEqual([
225
+ "main@git",
226
+ "release@origin",
227
+ ]);
228
+ });
229
+
230
+ test("preserves git remote bookmarks from colocated jj repositories", () => {
231
+ expect(parseJjRemoteBookmarkList('"main"\t"git"\n"release"\t"origin"\n')).toEqual([
232
+ "main@git",
233
+ "release@origin",
234
+ ]);
235
+ });
236
+ });