@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,195 @@
1
+ /**
2
+ * Note-taking app integrations (Obsidian, Bear, Octarine).
3
+ * Node.js equivalents of packages/server/integrations.ts.
4
+ * Config types, save functions, tag extraction, filename generation
5
+ */
6
+
7
+ import { execSync, spawn } from "node:child_process";
8
+ import { existsSync, mkdirSync, statSync, writeFileSync } from "node:fs";
9
+ import { basename, join } from "node:path";
10
+
11
+ import {
12
+ type ObsidianConfig,
13
+ type BearConfig,
14
+ type OctarineConfig,
15
+ type IntegrationResult,
16
+ extractTitle,
17
+ generateFrontmatter,
18
+ generateFilename,
19
+ generateOctarineFrontmatter,
20
+ stripH1,
21
+ buildHashtags,
22
+ buildBearContent,
23
+ detectObsidianVaults,
24
+ } from "../generated/integrations-common.js";
25
+ import { sanitizeTag } from "../generated/project.js";
26
+ import { resolveUserPath } from "../generated/resolve-file.js";
27
+
28
+ export type { ObsidianConfig, BearConfig, OctarineConfig, IntegrationResult };
29
+ export {
30
+ extractTitle,
31
+ generateFrontmatter,
32
+ generateFilename,
33
+ generateOctarineFrontmatter,
34
+ stripH1,
35
+ buildHashtags,
36
+ buildBearContent,
37
+ detectObsidianVaults,
38
+ };
39
+
40
+ /** Detect project name from git or cwd (sync). Used by extractTags for note integrations. */
41
+ function detectProjectNameSync(): string | null {
42
+ try {
43
+ const toplevel = execSync("git rev-parse --show-toplevel", {
44
+ encoding: "utf-8",
45
+ stdio: ["pipe", "pipe", "pipe"],
46
+ }).trim();
47
+ if (toplevel) {
48
+ const name = sanitizeTag(basename(toplevel));
49
+ if (name) return name;
50
+ }
51
+ } catch {
52
+ /* not in a git repo */
53
+ }
54
+ try {
55
+ return sanitizeTag(basename(process.cwd())) ?? null;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+
61
+ export async function extractTags(markdown: string): Promise<string[]> {
62
+ const tags = new Set<string>(["plannotator"]);
63
+ const projectName = detectProjectNameSync();
64
+ if (projectName) tags.add(projectName);
65
+ const stopWords = new Set([
66
+ "the",
67
+ "and",
68
+ "for",
69
+ "with",
70
+ "this",
71
+ "that",
72
+ "from",
73
+ "into",
74
+ "plan",
75
+ "implementation",
76
+ "overview",
77
+ "phase",
78
+ "step",
79
+ "steps",
80
+ ]);
81
+ const h1Match = markdown.match(
82
+ /^#\s+(?:Implementation\s+Plan:|Plan:)?\s*(.+)$/im,
83
+ );
84
+ if (h1Match) {
85
+ h1Match[1]
86
+ .toLowerCase()
87
+ .replace(/[^\w\s-]/g, " ")
88
+ .split(/\s+/)
89
+ .filter((w) => w.length > 2 && !stopWords.has(w))
90
+ .slice(0, 3)
91
+ .forEach((w) => tags.add(w));
92
+ }
93
+ const seenLangs = new Set<string>();
94
+ let langMatch: RegExpExecArray | null;
95
+ const langRegex = /```(\w+)/g;
96
+ while ((langMatch = langRegex.exec(markdown)) !== null) {
97
+ const lang = langMatch[1];
98
+ const n = lang.toLowerCase();
99
+ if (
100
+ !seenLangs.has(n) &&
101
+ !["json", "yaml", "yml", "text", "txt", "markdown", "md"].includes(n)
102
+ ) {
103
+ seenLangs.add(n);
104
+ tags.add(n);
105
+ }
106
+ }
107
+ return Array.from(tags).slice(0, 7);
108
+ }
109
+
110
+ export async function saveToObsidian(
111
+ config: ObsidianConfig,
112
+ ): Promise<IntegrationResult> {
113
+ try {
114
+ const { vaultPath, folder, plan } = config;
115
+ if (!vaultPath?.trim()) {
116
+ return { success: false, error: "Vault path is required" };
117
+ }
118
+ const normalizedVault = resolveUserPath(vaultPath);
119
+ if (!existsSync(normalizedVault))
120
+ return {
121
+ success: false,
122
+ error: `Vault path does not exist: ${normalizedVault}`,
123
+ };
124
+ if (!statSync(normalizedVault).isDirectory())
125
+ return {
126
+ success: false,
127
+ error: `Vault path is not a directory: ${normalizedVault}`,
128
+ };
129
+ const folderName = folder.trim() || "plannotator";
130
+ const targetFolder = join(normalizedVault, folderName);
131
+ if (!existsSync(targetFolder)) mkdirSync(targetFolder, { recursive: true });
132
+ const filename = generateFilename(
133
+ plan,
134
+ config.filenameFormat,
135
+ config.filenameSeparator,
136
+ );
137
+ const filePath = join(targetFolder, filename);
138
+ const tags = await extractTags(plan);
139
+ const frontmatter = generateFrontmatter(tags);
140
+ const content = `${frontmatter}\n\n[[Plannotator Plans]]\n\n${plan}`;
141
+ writeFileSync(filePath, content);
142
+ return { success: true, path: filePath };
143
+ } catch (err) {
144
+ return {
145
+ success: false,
146
+ error: err instanceof Error ? err.message : "Unknown error",
147
+ };
148
+ }
149
+ }
150
+
151
+ export async function saveToBear(
152
+ config: BearConfig,
153
+ ): Promise<IntegrationResult> {
154
+ try {
155
+ const { plan, customTags, tagPosition = "append" } = config;
156
+ const title = extractTitle(plan);
157
+ const body = stripH1(plan);
158
+ const tags = customTags?.trim() ? undefined : await extractTags(plan);
159
+ const hashtags = buildHashtags(customTags, tags ?? []);
160
+ const content = buildBearContent(body, hashtags, tagPosition);
161
+ const url = `bear://x-callback-url/create?title=${encodeURIComponent(title)}&text=${encodeURIComponent(content)}&open_note=no`;
162
+ spawn("open", [url], { stdio: "ignore" });
163
+ return { success: true };
164
+ } catch (err) {
165
+ return {
166
+ success: false,
167
+ error: err instanceof Error ? err.message : "Unknown error",
168
+ };
169
+ }
170
+ }
171
+
172
+ export async function saveToOctarine(
173
+ config: OctarineConfig,
174
+ ): Promise<IntegrationResult> {
175
+ try {
176
+ const { plan } = config;
177
+ const workspace = config.workspace.trim();
178
+ if (!workspace) return { success: false, error: "Workspace is required" };
179
+ const folder = config.folder.trim() || "plannotator";
180
+ const filename = generateFilename(plan);
181
+ const base = filename.replace(/\.md$/, "");
182
+ const path = folder ? `${folder}/${base}` : base;
183
+ const tags = await extractTags(plan);
184
+ const frontmatter = generateOctarineFrontmatter(tags);
185
+ const content = `${frontmatter}\n\n${plan}`;
186
+ const url = `octarine://create?path=${encodeURIComponent(path)}&content=${encodeURIComponent(content)}&workspace=${encodeURIComponent(workspace)}&fresh=true&openAfter=false`;
187
+ spawn("open", [url], { stdio: "ignore" });
188
+ return { success: true, path };
189
+ } catch (err) {
190
+ return {
191
+ success: false,
192
+ error: err instanceof Error ? err.message : "Unknown error",
193
+ };
194
+ }
195
+ }
@@ -0,0 +1,158 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ getServerHostname,
4
+ getServerPort,
5
+ isNoOpBrowserSentinel,
6
+ isRemoteSession,
7
+ openBrowser,
8
+ } from "./network";
9
+
10
+ const savedEnv: Record<string, string | undefined> = {};
11
+ const envKeys = [
12
+ "PLANNOTATOR_REMOTE",
13
+ "PLANNOTATOR_PORT",
14
+ "SSH_TTY",
15
+ "SSH_CONNECTION",
16
+ "PLANNOTATOR_BROWSER",
17
+ "BROWSER",
18
+ ];
19
+
20
+ function clearEnv() {
21
+ for (const key of envKeys) {
22
+ savedEnv[key] = process.env[key];
23
+ delete process.env[key];
24
+ }
25
+ }
26
+
27
+ afterEach(() => {
28
+ for (const key of envKeys) {
29
+ if (savedEnv[key] !== undefined) {
30
+ process.env[key] = savedEnv[key];
31
+ } else {
32
+ delete process.env[key];
33
+ }
34
+ }
35
+ });
36
+
37
+ describe("pi remote detection", () => {
38
+ test("false by default", () => {
39
+ clearEnv();
40
+ expect(isRemoteSession()).toBe(false);
41
+ });
42
+
43
+ test("true when PLANNOTATOR_REMOTE=1", () => {
44
+ clearEnv();
45
+ process.env.PLANNOTATOR_REMOTE = "1";
46
+ expect(isRemoteSession()).toBe(true);
47
+ });
48
+
49
+ test("true when PLANNOTATOR_REMOTE=true", () => {
50
+ clearEnv();
51
+ process.env.PLANNOTATOR_REMOTE = "true";
52
+ expect(isRemoteSession()).toBe(true);
53
+ });
54
+
55
+ test("false when PLANNOTATOR_REMOTE=0", () => {
56
+ clearEnv();
57
+ process.env.PLANNOTATOR_REMOTE = "0";
58
+ expect(isRemoteSession()).toBe(false);
59
+ });
60
+
61
+ test("false when PLANNOTATOR_REMOTE=false", () => {
62
+ clearEnv();
63
+ process.env.PLANNOTATOR_REMOTE = "false";
64
+ expect(isRemoteSession()).toBe(false);
65
+ });
66
+
67
+ test("PLANNOTATOR_REMOTE=false overrides SSH_TTY", () => {
68
+ clearEnv();
69
+ process.env.PLANNOTATOR_REMOTE = "false";
70
+ process.env.SSH_TTY = "/dev/pts/0";
71
+ expect(isRemoteSession()).toBe(false);
72
+ });
73
+
74
+ test("PLANNOTATOR_REMOTE=0 overrides SSH_CONNECTION", () => {
75
+ clearEnv();
76
+ process.env.PLANNOTATOR_REMOTE = "0";
77
+ process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22";
78
+ expect(isRemoteSession()).toBe(false);
79
+ });
80
+
81
+ test("true when SSH_TTY is set and env var is unset", () => {
82
+ clearEnv();
83
+ process.env.SSH_TTY = "/dev/pts/0";
84
+ expect(isRemoteSession()).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe("pi port selection", () => {
89
+ test("uses random local port when false overrides SSH", () => {
90
+ clearEnv();
91
+ process.env.PLANNOTATOR_REMOTE = "false";
92
+ process.env.SSH_TTY = "/dev/pts/0";
93
+ expect(getServerPort()).toEqual({ port: 0, portSource: "random" });
94
+ });
95
+
96
+ test("uses default remote port when SSH is detected", () => {
97
+ clearEnv();
98
+ process.env.SSH_CONNECTION = "192.168.1.1 12345 192.168.1.2 22";
99
+ expect(getServerPort()).toEqual({ port: 19432, portSource: "remote-default" });
100
+ });
101
+
102
+ test("PLANNOTATOR_PORT still takes precedence", () => {
103
+ clearEnv();
104
+ process.env.PLANNOTATOR_REMOTE = "false";
105
+ process.env.SSH_TTY = "/dev/pts/0";
106
+ process.env.PLANNOTATOR_PORT = "9999";
107
+ expect(getServerPort()).toEqual({ port: 9999, portSource: "env" });
108
+ });
109
+ });
110
+
111
+ describe("pi server hostname", () => {
112
+ test("binds local sessions to loopback", () => {
113
+ clearEnv();
114
+ expect(getServerHostname()).toBe("127.0.0.1");
115
+ });
116
+
117
+ test("binds remote sessions to all interfaces", () => {
118
+ clearEnv();
119
+ process.env.PLANNOTATOR_REMOTE = "1";
120
+ expect(getServerHostname()).toBe("0.0.0.0");
121
+ });
122
+ });
123
+
124
+ describe("pi browser no-op sentinels", () => {
125
+ test("recognizes no-op values case- and whitespace-insensitively", () => {
126
+ for (const value of [
127
+ "true",
128
+ "false",
129
+ "none",
130
+ ":",
131
+ "0",
132
+ "1",
133
+ "TRUE",
134
+ " none ",
135
+ ]) {
136
+ expect(isNoOpBrowserSentinel(value)).toBe(true);
137
+ }
138
+ });
139
+
140
+ test("does not flag real browser handlers or explicit command paths", () => {
141
+ expect(isNoOpBrowserSentinel("/usr/bin/firefox")).toBe(false);
142
+ expect(isNoOpBrowserSentinel("Google Chrome")).toBe(false);
143
+ expect(isNoOpBrowserSentinel("open")).toBe(false);
144
+ expect(isNoOpBrowserSentinel("/usr/bin/true")).toBe(false);
145
+ });
146
+
147
+ test("remote BROWSER=true is treated as no browser handler", async () => {
148
+ clearEnv();
149
+ process.env.PLANNOTATOR_REMOTE = "1";
150
+ process.env.BROWSER = "true";
151
+
152
+ expect(await openBrowser("http://127.0.0.1:19432")).toEqual({
153
+ opened: false,
154
+ isRemote: true,
155
+ url: "http://127.0.0.1:19432",
156
+ });
157
+ });
158
+ });
@@ -0,0 +1,268 @@
1
+ /**
2
+ * Network utilities — remote detection, port binding, browser opening.
3
+ * isRemoteSession, getServerPort, listenOnPort, openBrowser
4
+ */
5
+
6
+ import { spawn } from "node:child_process";
7
+ import { existsSync } from "node:fs";
8
+ import type { Server } from "node:http";
9
+ import { release } from "node:os";
10
+ import { delimiter, join } from "node:path";
11
+ import { loadConfig, resolveUseGlimpse } from "../generated/config.js";
12
+
13
+ const DEFAULT_REMOTE_PORT = 19432;
14
+ const LOOPBACK_HOST = "127.0.0.1";
15
+ const NOOP_BROWSER_VALUES = new Set(["true", "false", "none", ":", "0", "1"]);
16
+
17
+ export function isNoOpBrowserSentinel(value: string | undefined): boolean {
18
+ if (!value) return false;
19
+ return NOOP_BROWSER_VALUES.has(value.trim().toLowerCase());
20
+ }
21
+
22
+ /**
23
+ * Check if running in a remote session (SSH, devcontainer, etc.)
24
+ * Honors PLANNOTATOR_REMOTE as a tri-state override, or detects SSH_TTY/SSH_CONNECTION.
25
+ */
26
+ function getRemoteOverride(): boolean | null {
27
+ const remote = process.env.PLANNOTATOR_REMOTE;
28
+ if (remote === undefined) {
29
+ return null;
30
+ }
31
+
32
+ if (remote === "1" || remote?.toLowerCase() === "true") {
33
+ return true;
34
+ }
35
+
36
+ if (remote === "0" || remote?.toLowerCase() === "false") {
37
+ return false;
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ export function isRemoteSession(): boolean {
44
+ const remoteOverride = getRemoteOverride();
45
+ if (remoteOverride !== null) {
46
+ return remoteOverride;
47
+ }
48
+ // Legacy SSH detection
49
+ if (process.env.SSH_TTY || process.env.SSH_CONNECTION) {
50
+ return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /**
56
+ * Get the server port to use.
57
+ * - PLANNOTATOR_PORT env var takes precedence
58
+ * - Remote sessions default to 19432 (for port forwarding)
59
+ * - Local sessions use random port
60
+ * Returns { port, portSource } so caller can notify user if needed.
61
+ */
62
+ export function getServerPort(): {
63
+ port: number;
64
+ portSource: "env" | "remote-default" | "random";
65
+ } {
66
+ const envPort = process.env.PLANNOTATOR_PORT;
67
+ if (envPort) {
68
+ const parsed = parseInt(envPort, 10);
69
+ if (!Number.isNaN(parsed) && parsed >= 0 && parsed < 65536) {
70
+ return { port: parsed, portSource: "env" };
71
+ }
72
+ // Invalid port - fall back silently, caller can check env var themselves
73
+ }
74
+ if (isRemoteSession()) {
75
+ return { port: DEFAULT_REMOTE_PORT, portSource: "remote-default" };
76
+ }
77
+ return { port: 0, portSource: "random" };
78
+ }
79
+
80
+ export function getServerHostname(): string {
81
+ return isRemoteSession() ? "0.0.0.0" : LOOPBACK_HOST;
82
+ }
83
+
84
+ const MAX_RETRIES = 5;
85
+ const RETRY_DELAY_MS = 500;
86
+
87
+ export async function listenOnPort(
88
+ server: Server,
89
+ ): Promise<{ port: number; portSource: "env" | "remote-default" | "random" }> {
90
+ const result = getServerPort();
91
+
92
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
93
+ try {
94
+ await new Promise<void>((resolve, reject) => {
95
+ server.once("error", reject);
96
+ server.listen(
97
+ result.port,
98
+ getServerHostname(),
99
+ () => {
100
+ server.removeListener("error", reject);
101
+ resolve();
102
+ },
103
+ );
104
+ });
105
+ const addr = server.address() as { port: number };
106
+ return { port: addr.port, portSource: result.portSource };
107
+ } catch (err: unknown) {
108
+ const isAddressInUse =
109
+ err instanceof Error && err.message.includes("EADDRINUSE");
110
+ if (isAddressInUse && attempt < MAX_RETRIES) {
111
+ await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
112
+ continue;
113
+ }
114
+ if (isAddressInUse) {
115
+ const hint = isRemoteSession()
116
+ ? " (set PLANNOTATOR_PORT to use a different port)"
117
+ : "";
118
+ throw new Error(
119
+ `Port ${result.port} in use after ${MAX_RETRIES} retries${hint}`,
120
+ );
121
+ }
122
+ throw err;
123
+ }
124
+ }
125
+
126
+ // Unreachable, but satisfies TypeScript
127
+ throw new Error("Failed to bind port");
128
+ }
129
+
130
+ /**
131
+ * Open URL in system browser (Node-compatible, no Bun $ dependency).
132
+ * Honors PLANNOTATOR_BROWSER and BROWSER env vars.
133
+ * Returns { opened: true } if browser was opened, { opened: false, isRemote: true, url } if remote session.
134
+ */
135
+ function findCommandOnPath(command: string): string | null {
136
+ const extensions = process.platform === "win32" ? [".cmd", ".exe", ".bat", ""] : [""];
137
+ for (const dir of (process.env.PATH || "").split(delimiter)) {
138
+ if (!dir) continue;
139
+ for (const ext of extensions) {
140
+ const candidate = join(dir, `${command}${ext}`);
141
+ if (existsSync(candidate)) return candidate;
142
+ }
143
+ }
144
+ return null;
145
+ }
146
+
147
+ function buildGlimpseHtml(url: string): string {
148
+ const encodedUrl = JSON.stringify(url);
149
+ return `<!doctype html>
150
+ <html>
151
+ <head>
152
+ <meta charset="utf-8" />
153
+ <title>Plannotator</title>
154
+ <style>
155
+ html, body { width: 100%; height: 100%; margin: 0; }
156
+ body { overflow: hidden; background: #0f1115; }
157
+ </style>
158
+ </head>
159
+ <body>
160
+ <script>
161
+ location.replace(${encodedUrl});
162
+ </script>
163
+ </body>
164
+ </html>`;
165
+ }
166
+
167
+ async function openGlimpse(url: string): Promise<boolean> {
168
+ const glimpseCli = findCommandOnPath("glimpseui");
169
+ if (!glimpseCli) return false;
170
+
171
+ const args = [
172
+ "--width",
173
+ String(Number(process.env.PLANNOTATOR_GLIMPSE_WIDTH || 1280)),
174
+ "--height",
175
+ String(Number(process.env.PLANNOTATOR_GLIMPSE_HEIGHT || 900)),
176
+ "--title",
177
+ "Plannotator",
178
+ "--open-links",
179
+ ];
180
+ const html = buildGlimpseHtml(url);
181
+
182
+ return await new Promise<boolean>((resolve) => {
183
+ let settled = false;
184
+ let successTimer: ReturnType<typeof setTimeout> | undefined;
185
+ const finish = (opened: boolean) => {
186
+ if (settled) return;
187
+ settled = true;
188
+ if (successTimer) clearTimeout(successTimer);
189
+ resolve(opened);
190
+ };
191
+
192
+ const child = spawn(glimpseCli, args, {
193
+ detached: true,
194
+ stdio: ["pipe", "ignore", "ignore"],
195
+ });
196
+ successTimer = setTimeout(() => {
197
+ child.unref();
198
+ finish(true);
199
+ }, 750);
200
+
201
+ child.once("error", () => finish(false));
202
+ child.once("exit", () => finish(false));
203
+ child.stdin.once("error", () => finish(false));
204
+ child.stdin.end(html);
205
+ });
206
+ }
207
+
208
+ export async function openBrowser(url: string): Promise<{
209
+ opened: boolean;
210
+ isRemote?: boolean;
211
+ url?: string;
212
+ }> {
213
+ const rawPlannotatorBrowser = process.env.PLANNOTATOR_BROWSER;
214
+ const rawBrowser = process.env.BROWSER;
215
+ const plannotatorBrowser = isNoOpBrowserSentinel(rawPlannotatorBrowser)
216
+ ? undefined
217
+ : rawPlannotatorBrowser;
218
+ const envBrowser = isNoOpBrowserSentinel(rawBrowser) ? undefined : rawBrowser;
219
+ const browser = plannotatorBrowser || envBrowser;
220
+ if (isRemoteSession() && !browser) {
221
+ return { opened: false, isRemote: true, url };
222
+ }
223
+
224
+ if (!browser && resolveUseGlimpse(loadConfig())) {
225
+ const openedViaGlimpse = await openGlimpse(url);
226
+ if (openedViaGlimpse) {
227
+ return { opened: true };
228
+ }
229
+ }
230
+
231
+ try {
232
+ const platform = process.platform;
233
+ const wsl =
234
+ platform === "linux" && release().toLowerCase().includes("microsoft");
235
+
236
+ let cmd: string;
237
+ let args: string[];
238
+
239
+ if (browser) {
240
+ if (plannotatorBrowser && platform === "darwin") {
241
+ cmd = "open";
242
+ args = ["-a", plannotatorBrowser, url];
243
+ } else if ((platform === "win32" || wsl) && plannotatorBrowser) {
244
+ cmd = "cmd.exe";
245
+ args = ["/c", "start", "", plannotatorBrowser, url];
246
+ } else {
247
+ cmd = browser;
248
+ args = [url];
249
+ }
250
+ } else if (platform === "win32" || wsl) {
251
+ cmd = "cmd.exe";
252
+ args = ["/c", "start", "", url];
253
+ } else if (platform === "darwin") {
254
+ cmd = "open";
255
+ args = [url];
256
+ } else {
257
+ cmd = "xdg-open";
258
+ args = [url];
259
+ }
260
+
261
+ const child = spawn(cmd, args, { detached: true, stdio: "ignore" });
262
+ child.once("error", () => {});
263
+ child.unref();
264
+ return { opened: true };
265
+ } catch {
266
+ return { opened: false };
267
+ }
268
+ }