@oh-my-pi/pi-coding-agent 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 (337) hide show
  1. package/CHANGELOG.md +1629 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/config-usage.md +113 -0
  5. package/docs/custom-tools.md +541 -0
  6. package/docs/extension-loading.md +1004 -0
  7. package/docs/hooks.md +867 -0
  8. package/docs/rpc.md +1040 -0
  9. package/docs/sdk.md +994 -0
  10. package/docs/session-tree-plan.md +441 -0
  11. package/docs/session.md +240 -0
  12. package/docs/skills.md +290 -0
  13. package/docs/theme.md +670 -0
  14. package/docs/tree.md +197 -0
  15. package/docs/tui.md +341 -0
  16. package/examples/README.md +21 -0
  17. package/examples/custom-tools/README.md +124 -0
  18. package/examples/custom-tools/hello/index.ts +20 -0
  19. package/examples/custom-tools/question/index.ts +84 -0
  20. package/examples/custom-tools/subagent/README.md +172 -0
  21. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  22. package/examples/custom-tools/subagent/agents/scout.md +50 -0
  23. package/examples/custom-tools/subagent/agents/worker.md +24 -0
  24. package/examples/custom-tools/subagent/agents.ts +156 -0
  25. package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
  26. package/examples/custom-tools/subagent/commands/implement.md +10 -0
  27. package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
  28. package/examples/custom-tools/subagent/index.ts +1002 -0
  29. package/examples/custom-tools/todo/index.ts +212 -0
  30. package/examples/hooks/README.md +56 -0
  31. package/examples/hooks/auto-commit-on-exit.ts +49 -0
  32. package/examples/hooks/confirm-destructive.ts +59 -0
  33. package/examples/hooks/custom-compaction.ts +116 -0
  34. package/examples/hooks/dirty-repo-guard.ts +52 -0
  35. package/examples/hooks/file-trigger.ts +41 -0
  36. package/examples/hooks/git-checkpoint.ts +53 -0
  37. package/examples/hooks/handoff.ts +150 -0
  38. package/examples/hooks/permission-gate.ts +34 -0
  39. package/examples/hooks/protected-paths.ts +30 -0
  40. package/examples/hooks/qna.ts +119 -0
  41. package/examples/hooks/snake.ts +343 -0
  42. package/examples/hooks/status-line.ts +40 -0
  43. package/examples/sdk/01-minimal.ts +22 -0
  44. package/examples/sdk/02-custom-model.ts +49 -0
  45. package/examples/sdk/03-custom-prompt.ts +44 -0
  46. package/examples/sdk/04-skills.ts +44 -0
  47. package/examples/sdk/05-tools.ts +90 -0
  48. package/examples/sdk/06-hooks.ts +61 -0
  49. package/examples/sdk/07-context-files.ts +36 -0
  50. package/examples/sdk/08-slash-commands.ts +42 -0
  51. package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
  52. package/examples/sdk/10-settings.ts +38 -0
  53. package/examples/sdk/11-sessions.ts +48 -0
  54. package/examples/sdk/12-full-control.ts +95 -0
  55. package/examples/sdk/README.md +154 -0
  56. package/package.json +89 -0
  57. package/src/bun-imports.d.ts +16 -0
  58. package/src/capability/context-file.ts +40 -0
  59. package/src/capability/extension.ts +48 -0
  60. package/src/capability/hook.ts +40 -0
  61. package/src/capability/index.ts +616 -0
  62. package/src/capability/instruction.ts +37 -0
  63. package/src/capability/mcp.ts +52 -0
  64. package/src/capability/prompt.ts +35 -0
  65. package/src/capability/rule.ts +56 -0
  66. package/src/capability/settings.ts +35 -0
  67. package/src/capability/skill.ts +49 -0
  68. package/src/capability/slash-command.ts +40 -0
  69. package/src/capability/system-prompt.ts +35 -0
  70. package/src/capability/tool.ts +38 -0
  71. package/src/capability/types.ts +166 -0
  72. package/src/cli/args.ts +259 -0
  73. package/src/cli/file-processor.ts +121 -0
  74. package/src/cli/list-models.ts +104 -0
  75. package/src/cli/plugin-cli.ts +661 -0
  76. package/src/cli/session-picker.ts +41 -0
  77. package/src/cli/update-cli.ts +274 -0
  78. package/src/cli.ts +10 -0
  79. package/src/config.ts +391 -0
  80. package/src/core/agent-session.ts +2178 -0
  81. package/src/core/auth-storage.ts +258 -0
  82. package/src/core/bash-executor.ts +197 -0
  83. package/src/core/compaction/branch-summarization.ts +315 -0
  84. package/src/core/compaction/compaction.ts +664 -0
  85. package/src/core/compaction/index.ts +7 -0
  86. package/src/core/compaction/utils.ts +153 -0
  87. package/src/core/custom-commands/bundled/review/index.ts +156 -0
  88. package/src/core/custom-commands/index.ts +15 -0
  89. package/src/core/custom-commands/loader.ts +226 -0
  90. package/src/core/custom-commands/types.ts +112 -0
  91. package/src/core/custom-tools/index.ts +22 -0
  92. package/src/core/custom-tools/loader.ts +248 -0
  93. package/src/core/custom-tools/types.ts +185 -0
  94. package/src/core/custom-tools/wrapper.ts +29 -0
  95. package/src/core/exec.ts +139 -0
  96. package/src/core/export-html/index.ts +159 -0
  97. package/src/core/export-html/template.css +774 -0
  98. package/src/core/export-html/template.generated.ts +2 -0
  99. package/src/core/export-html/template.html +45 -0
  100. package/src/core/export-html/template.js +1185 -0
  101. package/src/core/export-html/template.macro.ts +24 -0
  102. package/src/core/file-mentions.ts +54 -0
  103. package/src/core/hooks/index.ts +16 -0
  104. package/src/core/hooks/loader.ts +288 -0
  105. package/src/core/hooks/runner.ts +434 -0
  106. package/src/core/hooks/tool-wrapper.ts +98 -0
  107. package/src/core/hooks/types.ts +770 -0
  108. package/src/core/index.ts +53 -0
  109. package/src/core/logger.ts +112 -0
  110. package/src/core/mcp/client.ts +185 -0
  111. package/src/core/mcp/config.ts +248 -0
  112. package/src/core/mcp/index.ts +45 -0
  113. package/src/core/mcp/loader.ts +99 -0
  114. package/src/core/mcp/manager.ts +235 -0
  115. package/src/core/mcp/tool-bridge.ts +156 -0
  116. package/src/core/mcp/transports/http.ts +316 -0
  117. package/src/core/mcp/transports/index.ts +6 -0
  118. package/src/core/mcp/transports/stdio.ts +252 -0
  119. package/src/core/mcp/types.ts +228 -0
  120. package/src/core/messages.ts +211 -0
  121. package/src/core/model-registry.ts +334 -0
  122. package/src/core/model-resolver.ts +494 -0
  123. package/src/core/plugins/doctor.ts +67 -0
  124. package/src/core/plugins/index.ts +38 -0
  125. package/src/core/plugins/installer.ts +189 -0
  126. package/src/core/plugins/loader.ts +339 -0
  127. package/src/core/plugins/manager.ts +672 -0
  128. package/src/core/plugins/parser.ts +105 -0
  129. package/src/core/plugins/paths.ts +37 -0
  130. package/src/core/plugins/types.ts +190 -0
  131. package/src/core/sdk.ts +900 -0
  132. package/src/core/session-manager.ts +1837 -0
  133. package/src/core/settings-manager.ts +860 -0
  134. package/src/core/skills.ts +352 -0
  135. package/src/core/slash-commands.ts +132 -0
  136. package/src/core/system-prompt.ts +442 -0
  137. package/src/core/timings.ts +25 -0
  138. package/src/core/title-generator.ts +110 -0
  139. package/src/core/tools/ask.ts +193 -0
  140. package/src/core/tools/bash-interceptor.ts +120 -0
  141. package/src/core/tools/bash.ts +91 -0
  142. package/src/core/tools/context.ts +32 -0
  143. package/src/core/tools/edit-diff.ts +487 -0
  144. package/src/core/tools/edit.ts +140 -0
  145. package/src/core/tools/exa/company.ts +59 -0
  146. package/src/core/tools/exa/index.ts +63 -0
  147. package/src/core/tools/exa/linkedin.ts +59 -0
  148. package/src/core/tools/exa/mcp-client.ts +368 -0
  149. package/src/core/tools/exa/render.ts +200 -0
  150. package/src/core/tools/exa/researcher.ts +90 -0
  151. package/src/core/tools/exa/search.ts +338 -0
  152. package/src/core/tools/exa/types.ts +167 -0
  153. package/src/core/tools/exa/websets.ts +248 -0
  154. package/src/core/tools/find.ts +244 -0
  155. package/src/core/tools/grep.ts +584 -0
  156. package/src/core/tools/index.ts +283 -0
  157. package/src/core/tools/ls.ts +142 -0
  158. package/src/core/tools/lsp/client.ts +767 -0
  159. package/src/core/tools/lsp/clients/biome-client.ts +207 -0
  160. package/src/core/tools/lsp/clients/index.ts +49 -0
  161. package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
  162. package/src/core/tools/lsp/config.ts +845 -0
  163. package/src/core/tools/lsp/edits.ts +110 -0
  164. package/src/core/tools/lsp/index.ts +1364 -0
  165. package/src/core/tools/lsp/render.ts +560 -0
  166. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  167. package/src/core/tools/lsp/types.ts +495 -0
  168. package/src/core/tools/lsp/utils.ts +526 -0
  169. package/src/core/tools/notebook.ts +182 -0
  170. package/src/core/tools/output.ts +198 -0
  171. package/src/core/tools/path-utils.ts +61 -0
  172. package/src/core/tools/read.ts +507 -0
  173. package/src/core/tools/renderers.ts +820 -0
  174. package/src/core/tools/review.ts +275 -0
  175. package/src/core/tools/rulebook.ts +124 -0
  176. package/src/core/tools/task/agents.ts +158 -0
  177. package/src/core/tools/task/artifacts.ts +114 -0
  178. package/src/core/tools/task/commands.ts +157 -0
  179. package/src/core/tools/task/discovery.ts +217 -0
  180. package/src/core/tools/task/executor.ts +531 -0
  181. package/src/core/tools/task/index.ts +548 -0
  182. package/src/core/tools/task/model-resolver.ts +176 -0
  183. package/src/core/tools/task/parallel.ts +38 -0
  184. package/src/core/tools/task/render.ts +502 -0
  185. package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
  186. package/src/core/tools/task/types.ts +142 -0
  187. package/src/core/tools/truncate.ts +265 -0
  188. package/src/core/tools/web-fetch.ts +2511 -0
  189. package/src/core/tools/web-search/auth.ts +199 -0
  190. package/src/core/tools/web-search/index.ts +583 -0
  191. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  192. package/src/core/tools/web-search/providers/exa.ts +196 -0
  193. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  194. package/src/core/tools/web-search/render.ts +372 -0
  195. package/src/core/tools/web-search/types.ts +180 -0
  196. package/src/core/tools/write.ts +63 -0
  197. package/src/core/ttsr.ts +211 -0
  198. package/src/core/utils.ts +187 -0
  199. package/src/discovery/agents-md.ts +75 -0
  200. package/src/discovery/builtin.ts +647 -0
  201. package/src/discovery/claude.ts +623 -0
  202. package/src/discovery/cline.ts +104 -0
  203. package/src/discovery/codex.ts +571 -0
  204. package/src/discovery/cursor.ts +266 -0
  205. package/src/discovery/gemini.ts +368 -0
  206. package/src/discovery/github.ts +120 -0
  207. package/src/discovery/helpers.test.ts +127 -0
  208. package/src/discovery/helpers.ts +249 -0
  209. package/src/discovery/index.ts +84 -0
  210. package/src/discovery/mcp-json.ts +127 -0
  211. package/src/discovery/vscode.ts +99 -0
  212. package/src/discovery/windsurf.ts +219 -0
  213. package/src/index.ts +192 -0
  214. package/src/main.ts +507 -0
  215. package/src/migrations.ts +156 -0
  216. package/src/modes/cleanup.ts +23 -0
  217. package/src/modes/index.ts +48 -0
  218. package/src/modes/interactive/components/armin.ts +382 -0
  219. package/src/modes/interactive/components/assistant-message.ts +86 -0
  220. package/src/modes/interactive/components/bash-execution.ts +199 -0
  221. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  222. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  223. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  224. package/src/modes/interactive/components/custom-editor.ts +122 -0
  225. package/src/modes/interactive/components/diff.ts +147 -0
  226. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  227. package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
  228. package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
  229. package/src/modes/interactive/components/extensions/index.ts +9 -0
  230. package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
  231. package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
  232. package/src/modes/interactive/components/extensions/types.ts +191 -0
  233. package/src/modes/interactive/components/hook-editor.ts +117 -0
  234. package/src/modes/interactive/components/hook-input.ts +64 -0
  235. package/src/modes/interactive/components/hook-message.ts +96 -0
  236. package/src/modes/interactive/components/hook-selector.ts +91 -0
  237. package/src/modes/interactive/components/model-selector.ts +560 -0
  238. package/src/modes/interactive/components/oauth-selector.ts +136 -0
  239. package/src/modes/interactive/components/plugin-settings.ts +481 -0
  240. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  241. package/src/modes/interactive/components/session-selector.ts +220 -0
  242. package/src/modes/interactive/components/settings-defs.ts +597 -0
  243. package/src/modes/interactive/components/settings-selector.ts +545 -0
  244. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  245. package/src/modes/interactive/components/status-line/index.ts +4 -0
  246. package/src/modes/interactive/components/status-line/presets.ts +94 -0
  247. package/src/modes/interactive/components/status-line/segments.ts +350 -0
  248. package/src/modes/interactive/components/status-line/separators.ts +55 -0
  249. package/src/modes/interactive/components/status-line/types.ts +81 -0
  250. package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
  251. package/src/modes/interactive/components/status-line.ts +384 -0
  252. package/src/modes/interactive/components/theme-selector.ts +62 -0
  253. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  254. package/src/modes/interactive/components/tool-execution.ts +946 -0
  255. package/src/modes/interactive/components/tree-selector.ts +877 -0
  256. package/src/modes/interactive/components/ttsr-notification.ts +82 -0
  257. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  258. package/src/modes/interactive/components/user-message.ts +18 -0
  259. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  260. package/src/modes/interactive/components/welcome.ts +228 -0
  261. package/src/modes/interactive/interactive-mode.ts +2669 -0
  262. package/src/modes/interactive/theme/dark.json +102 -0
  263. package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
  264. package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
  265. package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
  266. package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
  267. package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
  268. package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
  269. package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
  270. package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
  271. package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
  272. package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
  273. package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
  274. package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
  275. package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
  276. package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
  277. package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
  278. package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
  279. package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
  280. package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
  281. package/src/modes/interactive/theme/defaults/index.ts +67 -0
  282. package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
  283. package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
  284. package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
  285. package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
  286. package/src/modes/interactive/theme/defaults/light-github.json +114 -0
  287. package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
  288. package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
  289. package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
  290. package/src/modes/interactive/theme/defaults/light-one.json +105 -0
  291. package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
  292. package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
  293. package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
  294. package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
  295. package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
  296. package/src/modes/interactive/theme/light.json +99 -0
  297. package/src/modes/interactive/theme/theme-schema.json +424 -0
  298. package/src/modes/interactive/theme/theme.ts +2211 -0
  299. package/src/modes/print-mode.ts +163 -0
  300. package/src/modes/rpc/rpc-client.ts +527 -0
  301. package/src/modes/rpc/rpc-mode.ts +494 -0
  302. package/src/modes/rpc/rpc-types.ts +203 -0
  303. package/src/prompts/architect-plan.md +10 -0
  304. package/src/prompts/branch-summary-preamble.md +3 -0
  305. package/src/prompts/branch-summary.md +28 -0
  306. package/src/prompts/browser.md +71 -0
  307. package/src/prompts/compaction-summary.md +34 -0
  308. package/src/prompts/compaction-turn-prefix.md +16 -0
  309. package/src/prompts/compaction-update-summary.md +41 -0
  310. package/src/prompts/explore.md +82 -0
  311. package/src/prompts/implement-with-critic.md +11 -0
  312. package/src/prompts/implement.md +11 -0
  313. package/src/prompts/init.md +30 -0
  314. package/src/prompts/plan.md +54 -0
  315. package/src/prompts/reviewer.md +81 -0
  316. package/src/prompts/summarization-system.md +3 -0
  317. package/src/prompts/system-prompt.md +27 -0
  318. package/src/prompts/task.md +56 -0
  319. package/src/prompts/title-system.md +8 -0
  320. package/src/prompts/tools/ask.md +24 -0
  321. package/src/prompts/tools/bash.md +23 -0
  322. package/src/prompts/tools/edit.md +9 -0
  323. package/src/prompts/tools/find.md +6 -0
  324. package/src/prompts/tools/grep.md +12 -0
  325. package/src/prompts/tools/lsp.md +14 -0
  326. package/src/prompts/tools/output.md +23 -0
  327. package/src/prompts/tools/read.md +25 -0
  328. package/src/prompts/tools/web-fetch.md +8 -0
  329. package/src/prompts/tools/web-search.md +10 -0
  330. package/src/prompts/tools/write.md +10 -0
  331. package/src/utils/changelog.ts +99 -0
  332. package/src/utils/clipboard.ts +265 -0
  333. package/src/utils/fuzzy.ts +108 -0
  334. package/src/utils/mime.ts +30 -0
  335. package/src/utils/shell-snapshot.ts +218 -0
  336. package/src/utils/shell.ts +364 -0
  337. package/src/utils/tools-manager.ts +265 -0
@@ -0,0 +1,442 @@
1
+ /**
2
+ * System prompt construction and project context loading
3
+ */
4
+
5
+ import { existsSync, readFileSync } from "node:fs";
6
+ import chalk from "chalk";
7
+ import { contextFileCapability } from "../capability/context-file";
8
+ import type { Rule } from "../capability/rule";
9
+ import { systemPromptCapability } from "../capability/system-prompt";
10
+ import { getDocsPath, getExamplesPath, getReadmePath } from "../config";
11
+ import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
12
+ import systemPromptTemplate from "../prompts/system-prompt.md" with { type: "text" };
13
+ import type { SkillsSettings } from "./settings-manager";
14
+ import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
15
+ import type { ToolName } from "./tools/index";
16
+ import { formatRulesForPrompt } from "./tools/rulebook";
17
+
18
+ /**
19
+ * Execute a git command synchronously and return stdout or null on failure.
20
+ */
21
+ function execGit(args: string[], cwd: string): string | null {
22
+ const result = Bun.spawnSync(["git", ...args], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
23
+ if (result.exitCode !== 0) return null;
24
+ return result.stdout.toString().trim() || null;
25
+ }
26
+
27
+ /**
28
+ * Load git context for the system prompt.
29
+ * Returns formatted git status or null if not in a git repo.
30
+ */
31
+ export function loadGitContext(cwd: string): string | null {
32
+ // Check if inside a git repo
33
+ const isGitRepo = execGit(["rev-parse", "--is-inside-work-tree"], cwd);
34
+ if (isGitRepo !== "true") return null;
35
+
36
+ // Get current branch
37
+ const currentBranch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
38
+ if (!currentBranch) return null;
39
+
40
+ // Detect main branch (check for 'main' first, then 'master')
41
+ let mainBranch = "main";
42
+ const mainExists = execGit(["rev-parse", "--verify", "main"], cwd);
43
+ if (mainExists === null) {
44
+ const masterExists = execGit(["rev-parse", "--verify", "master"], cwd);
45
+ if (masterExists !== null) mainBranch = "master";
46
+ }
47
+
48
+ // Get git status (porcelain format for parsing)
49
+ const gitStatus = execGit(["status", "--porcelain"], cwd);
50
+ const statusText = gitStatus?.trim() || "(clean)";
51
+
52
+ // Get recent commits
53
+ const recentCommits = execGit(["log", "--oneline", "-5"], cwd);
54
+ const commitsText = recentCommits?.trim() || "(no commits)";
55
+
56
+ return `This is the git status at the start of the conversation. Note that this status is a snapshot in time, and will not update during the conversation.
57
+ Current branch: ${currentBranch}
58
+
59
+ Main branch (you will usually use this for PRs): ${mainBranch}
60
+
61
+ Status:
62
+ ${statusText}
63
+
64
+ Recent commits:
65
+ ${commitsText}`;
66
+ }
67
+
68
+ /** Tool descriptions for system prompt */
69
+ const toolDescriptions: Record<ToolName, string> = {
70
+ ask: "Ask user for input or clarification",
71
+ read: "Read file contents",
72
+ bash: "Execute bash commands (git, npm, docker, etc.)",
73
+ edit: "Make surgical edits to files (find exact text and replace)",
74
+ write: "Create or overwrite files",
75
+ grep: "Search file contents for patterns (respects .gitignore)",
76
+ find: "Find files by glob pattern (respects .gitignore)",
77
+ ls: "List directory contents",
78
+ lsp: "PREFERRED for semantic code queries: go-to-definition, find-all-references, hover (type info), call hierarchy. Returns precise, deterministic results. Use BEFORE grep for symbol lookups.",
79
+ notebook: "Edit Jupyter notebook cells",
80
+ task: "Spawn a sub-agent to handle complex tasks",
81
+ web_fetch: "Fetch and render URLs into clean text for LLM consumption",
82
+ web_search: "Search the web for information",
83
+ };
84
+
85
+ /**
86
+ * Generate anti-bash rules section if the agent has both bash and specialized tools.
87
+ * Only include rules for tools that are actually available.
88
+ */
89
+ function generateAntiBashRules(tools: ToolName[]): string | null {
90
+ const hasBash = tools.includes("bash");
91
+ if (!hasBash) return null;
92
+
93
+ const hasRead = tools.includes("read");
94
+ const hasGrep = tools.includes("grep");
95
+ const hasFind = tools.includes("find");
96
+ const hasLs = tools.includes("ls");
97
+ const hasEdit = tools.includes("edit");
98
+ const hasLsp = tools.includes("lsp");
99
+
100
+ // Only show rules if we have specialized tools that should be preferred
101
+ const hasSpecializedTools = hasRead || hasGrep || hasFind || hasLs || hasEdit;
102
+ if (!hasSpecializedTools) return null;
103
+
104
+ const lines: string[] = [];
105
+ lines.push("## Tool Usage Rules — MANDATORY\n");
106
+ lines.push("### Forbidden Bash Patterns");
107
+ lines.push("NEVER use bash for these operations:\n");
108
+
109
+ if (hasRead) lines.push("- **File reading**: Use `read` instead of cat/head/tail/less/more");
110
+ if (hasGrep) lines.push("- **Content search**: Use `grep` instead of grep/rg/ag/ack");
111
+ if (hasFind) lines.push("- **File finding**: Use `find` instead of find/fd/locate");
112
+ if (hasLs) lines.push("- **Directory listing**: Use `ls` instead of bash ls");
113
+ if (hasEdit) lines.push("- **File editing**: Use `edit` instead of sed/awk/perl -pi/echo >/cat <<EOF");
114
+
115
+ lines.push("\n### Tool Preference (highest → lowest priority)");
116
+ const ladder: string[] = [];
117
+ if (hasLsp) ladder.push("lsp (go-to-definition, references, type info) — DETERMINISTIC");
118
+ if (hasGrep) ladder.push("grep (text/regex search)");
119
+ if (hasFind) ladder.push("find (locate files by pattern)");
120
+ if (hasRead) ladder.push("read (view file contents)");
121
+ if (hasEdit) ladder.push("edit (precise text replacement)");
122
+ ladder.push("bash (ONLY for git, npm, docker, make, cargo, etc.)");
123
+ lines.push(ladder.map((t, i) => `${i + 1}. ${t}`).join("\n"));
124
+
125
+ // Add LSP guidance if available
126
+ if (hasLsp) {
127
+ lines.push("\n### LSP — Preferred for Semantic Queries");
128
+ lines.push("Use `lsp` instead of grep/bash when you need:");
129
+ lines.push("- **Where is X defined?** → `lsp definition`");
130
+ lines.push("- **What calls X?** → `lsp incoming_calls`");
131
+ lines.push("- **What does X call?** → `lsp outgoing_calls`");
132
+ lines.push("- **What type is X?** → `lsp hover`");
133
+ lines.push("- **What symbols are in this file?** → `lsp symbols`");
134
+ lines.push("- **Find symbol across codebase** → `lsp workspace_symbols`\n");
135
+ }
136
+
137
+ // Add search-first protocol
138
+ if (hasGrep || hasFind) {
139
+ lines.push("\n### Search-First Protocol");
140
+ lines.push("Before reading any file:");
141
+ if (hasFind) lines.push("1. Unknown structure → `find` to see file layout");
142
+ if (hasGrep) lines.push("2. Known location → `grep` for specific symbol/error");
143
+ if (hasRead) lines.push("3. Use `read offset/limit` for line ranges, not entire large files");
144
+ lines.push("4. Never read a large file hoping to find something — search first");
145
+ }
146
+
147
+ return lines.join("\n");
148
+ }
149
+
150
+ /** Resolve input as file path or literal string */
151
+ export function resolvePromptInput(input: string | undefined, description: string): string | undefined {
152
+ if (!input) {
153
+ return undefined;
154
+ }
155
+
156
+ if (existsSync(input)) {
157
+ try {
158
+ return readFileSync(input, "utf-8");
159
+ } catch (error) {
160
+ console.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));
161
+ return input;
162
+ }
163
+ }
164
+
165
+ return input;
166
+ }
167
+
168
+ export interface LoadContextFilesOptions {
169
+ /** Working directory to start walking up from. Default: process.cwd() */
170
+ cwd?: string;
171
+ }
172
+
173
+ /**
174
+ * Load all project context files using the capability API.
175
+ * Returns {path, content, depth} entries for all discovered context files.
176
+ * Files are sorted by depth (descending) so files closer to cwd appear last/more prominent.
177
+ */
178
+ export function loadProjectContextFiles(
179
+ options: LoadContextFilesOptions = {},
180
+ ): Array<{ path: string; content: string; depth?: number }> {
181
+ const resolvedCwd = options.cwd ?? process.cwd();
182
+
183
+ const result = loadSync(contextFileCapability.id, { cwd: resolvedCwd });
184
+
185
+ // Convert ContextFile items and preserve depth info
186
+ const files = result.items.map((item) => {
187
+ const contextFile = item as ContextFile;
188
+ return {
189
+ path: contextFile.path,
190
+ content: contextFile.content,
191
+ depth: contextFile.depth,
192
+ };
193
+ });
194
+
195
+ // Sort by depth (descending): higher depth (farther from cwd) comes first,
196
+ // so files closer to cwd appear later and are more prominent
197
+ files.sort((a, b) => {
198
+ const depthA = a.depth ?? -1;
199
+ const depthB = b.depth ?? -1;
200
+ return depthB - depthA;
201
+ });
202
+
203
+ return files;
204
+ }
205
+
206
+ /**
207
+ * Load system prompt customization files (SYSTEM.md).
208
+ * Returns combined content from all discovered SYSTEM.md files.
209
+ */
210
+ export function loadSystemPromptFiles(options: LoadContextFilesOptions = {}): string | null {
211
+ const resolvedCwd = options.cwd ?? process.cwd();
212
+
213
+ const result = loadSync<SystemPromptFile>(systemPromptCapability.id, { cwd: resolvedCwd });
214
+
215
+ if (result.items.length === 0) return null;
216
+
217
+ // Combine all SYSTEM.md contents (user-level first, then project-level)
218
+ const userLevel = result.items.filter((item) => item.level === "user");
219
+ const projectLevel = result.items.filter((item) => item.level === "project");
220
+
221
+ const parts: string[] = [];
222
+ for (const item of [...userLevel, ...projectLevel]) {
223
+ parts.push(item.content);
224
+ }
225
+
226
+ return parts.join("\n\n");
227
+ }
228
+
229
+ export interface BuildSystemPromptOptions {
230
+ /** Custom system prompt (replaces default). */
231
+ customPrompt?: string;
232
+ /** Tools to include in prompt. Default: [read, bash, edit, write] */
233
+ selectedTools?: ToolName[];
234
+ /** Text to append to system prompt. */
235
+ appendSystemPrompt?: string;
236
+ /** Skills settings for discovery. */
237
+ skillsSettings?: SkillsSettings;
238
+ /** Working directory. Default: process.cwd() */
239
+ cwd?: string;
240
+ /** Pre-loaded context files (skips discovery if provided). */
241
+ contextFiles?: Array<{ path: string; content: string; depth?: number }>;
242
+ /** Pre-loaded skills (skips discovery if provided). */
243
+ skills?: Skill[];
244
+ /** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
245
+ rulebookRules?: Rule[];
246
+ }
247
+
248
+ /** Build the system prompt with tools, guidelines, and context */
249
+ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
250
+ const {
251
+ customPrompt,
252
+ selectedTools,
253
+ appendSystemPrompt,
254
+ skillsSettings,
255
+ cwd,
256
+ contextFiles: providedContextFiles,
257
+ skills: providedSkills,
258
+ rulebookRules,
259
+ } = options;
260
+ const resolvedCwd = cwd ?? process.cwd();
261
+ const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
262
+ const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
263
+
264
+ // Load SYSTEM.md customization (prepended to prompt)
265
+ const systemPromptCustomization = loadSystemPromptFiles({ cwd: resolvedCwd });
266
+
267
+ const now = new Date();
268
+ const dateTime = now.toLocaleString("en-US", {
269
+ weekday: "long",
270
+ year: "numeric",
271
+ month: "long",
272
+ day: "numeric",
273
+ hour: "2-digit",
274
+ minute: "2-digit",
275
+ second: "2-digit",
276
+ timeZoneName: "short",
277
+ });
278
+
279
+ const appendSection = resolvedAppendPrompt ? `\n\n${resolvedAppendPrompt}` : "";
280
+
281
+ // Resolve context files: use provided or discover
282
+ const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd });
283
+
284
+ // Resolve skills: use provided or discover
285
+ const skills =
286
+ providedSkills ??
287
+ (skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd }).skills : []);
288
+
289
+ if (resolvedCustomPrompt) {
290
+ let prompt = systemPromptCustomization
291
+ ? `${systemPromptCustomization}\n\n${resolvedCustomPrompt}`
292
+ : resolvedCustomPrompt;
293
+
294
+ if (appendSection) {
295
+ prompt += appendSection;
296
+ }
297
+
298
+ // Append project context files
299
+ if (contextFiles.length > 0) {
300
+ prompt += "\n\n# Project Context\n\n";
301
+ prompt += "The following project context files have been loaded:\n\n";
302
+ for (const { path: filePath, content } of contextFiles) {
303
+ prompt += `## ${filePath}\n\n${content}\n\n`;
304
+ }
305
+ }
306
+
307
+ // Append git context if in a git repo
308
+ const gitContext = loadGitContext(resolvedCwd);
309
+ if (gitContext) {
310
+ prompt += `\n\n# Git Status\n\n${gitContext}`;
311
+ }
312
+
313
+ // Append skills section (only if read tool is available)
314
+ const customPromptHasRead = !selectedTools || selectedTools.includes("read");
315
+ if (customPromptHasRead && skills.length > 0) {
316
+ prompt += formatSkillsForPrompt(skills);
317
+ }
318
+
319
+ // Append rules section (always enabled when rules exist)
320
+ if (rulebookRules && rulebookRules.length > 0) {
321
+ prompt += formatRulesForPrompt(rulebookRules);
322
+ }
323
+
324
+ // Add date/time and working directory last
325
+ prompt += `\nCurrent date and time: ${dateTime}`;
326
+ prompt += `\nCurrent working directory: ${resolvedCwd}`;
327
+
328
+ return prompt;
329
+ }
330
+
331
+ // Get absolute paths to documentation and examples
332
+ const readmePath = getReadmePath();
333
+ const docsPath = getDocsPath();
334
+ const examplesPath = getExamplesPath();
335
+
336
+ // Build tools list based on selected tools
337
+ const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);
338
+ const toolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n");
339
+
340
+ // Generate anti-bash rules (returns null if not applicable)
341
+ const antiBashSection = generateAntiBashRules(tools);
342
+
343
+ // Build guidelines based on which tools are actually available
344
+ const guidelinesList: string[] = [];
345
+
346
+ const hasBash = tools.includes("bash");
347
+ const hasEdit = tools.includes("edit");
348
+ const hasWrite = tools.includes("write");
349
+ const hasRead = tools.includes("read");
350
+
351
+ // Read-only mode notice (no bash, edit, or write)
352
+ if (!hasBash && !hasEdit && !hasWrite) {
353
+ guidelinesList.push("You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands");
354
+ }
355
+
356
+ // Bash without edit/write = read-only bash mode
357
+ if (hasBash && !hasEdit && !hasWrite) {
358
+ guidelinesList.push(
359
+ "Use bash ONLY for read-only operations (git log, gh issue view, curl, etc.) - do NOT modify any files",
360
+ );
361
+ }
362
+
363
+ // Read before edit guideline
364
+ if (hasRead && hasEdit) {
365
+ guidelinesList.push("Use read to examine files before editing");
366
+ }
367
+
368
+ // Edit guideline
369
+ if (hasEdit) {
370
+ guidelinesList.push(
371
+ "Use edit for precise changes (old text must match exactly, fuzzy matching handles whitespace)",
372
+ );
373
+ }
374
+
375
+ // Write guideline
376
+ if (hasWrite) {
377
+ guidelinesList.push("Use write only for new files or complete rewrites");
378
+ }
379
+
380
+ // Output guideline (only when actually writing/executing)
381
+ if (hasEdit || hasWrite) {
382
+ guidelinesList.push(
383
+ "When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
384
+ );
385
+ }
386
+
387
+ // Always include these
388
+ guidelinesList.push("Be concise in your responses");
389
+ guidelinesList.push("Show file paths clearly when working with files");
390
+
391
+ const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
392
+
393
+ // Build the prompt with anti-bash rules prominently placed
394
+ const antiBashBlock = antiBashSection ? `\n${antiBashSection}\n` : "";
395
+ let prompt = systemPromptTemplate
396
+ .replaceAll("{{toolsList}}", toolsList)
397
+ .replaceAll("{{antiBashSection}}", antiBashBlock)
398
+ .replaceAll("{{guidelines}}", guidelines)
399
+ .replaceAll("{{readmePath}}", readmePath)
400
+ .replaceAll("{{docsPath}}", docsPath)
401
+ .replaceAll("{{examplesPath}}", examplesPath);
402
+
403
+ if (appendSection) {
404
+ prompt += appendSection;
405
+ }
406
+
407
+ // Append project context files
408
+ if (contextFiles.length > 0) {
409
+ prompt += "\n\n# Project Context\n\n";
410
+ prompt += "The following project context files have been loaded:\n\n";
411
+ for (const { path: filePath, content } of contextFiles) {
412
+ prompt += `## ${filePath}\n\n${content}\n\n`;
413
+ }
414
+ }
415
+
416
+ // Append git context if in a git repo
417
+ const gitContext = loadGitContext(resolvedCwd);
418
+ if (gitContext) {
419
+ prompt += `\n\n# Git Status\n\n${gitContext}`;
420
+ }
421
+
422
+ // Append skills section (only if read tool is available)
423
+ if (hasRead && skills.length > 0) {
424
+ prompt += formatSkillsForPrompt(skills);
425
+ }
426
+
427
+ // Append rules section (always enabled when rules exist)
428
+ if (rulebookRules && rulebookRules.length > 0) {
429
+ prompt += formatRulesForPrompt(rulebookRules);
430
+ }
431
+
432
+ // Add date/time and working directory last
433
+ prompt += `\nCurrent date and time: ${dateTime}`;
434
+ prompt += `\nCurrent working directory: ${resolvedCwd}`;
435
+
436
+ // Prepend SYSTEM.md customization if present
437
+ if (systemPromptCustomization) {
438
+ prompt = `${systemPromptCustomization}\n\n${prompt}`;
439
+ }
440
+
441
+ return prompt;
442
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Central timing instrumentation for startup profiling.
3
+ * Enable with OMP_TIMING=1 or PI_TIMING=1 environment variable.
4
+ */
5
+
6
+ const ENABLED = process.env.OMP_TIMING === "1" || process.env.PI_TIMING === "1";
7
+ const timings: Array<{ label: string; ms: number }> = [];
8
+ let lastTime = Date.now();
9
+
10
+ export function time(label: string): void {
11
+ if (!ENABLED) return;
12
+ const now = Date.now();
13
+ timings.push({ label, ms: now - lastTime });
14
+ lastTime = now;
15
+ }
16
+
17
+ export function printTimings(): void {
18
+ if (!ENABLED || timings.length === 0) return;
19
+ console.error("\n--- Startup Timings ---");
20
+ for (const t of timings) {
21
+ console.error(` ${t.label}: ${t.ms}ms`);
22
+ }
23
+ console.error(` TOTAL: ${timings.reduce((a, b) => a + b.ms, 0)}ms`);
24
+ console.error("------------------------\n");
25
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Generate session titles using a smol, fast model.
3
+ */
4
+
5
+ import type { Model } from "@oh-my-pi/pi-ai";
6
+ import { completeSimple } from "@oh-my-pi/pi-ai";
7
+ import titleSystemPrompt from "../prompts/title-system.md" with { type: "text" };
8
+ import { logger } from "./logger";
9
+ import type { ModelRegistry } from "./model-registry";
10
+ import { findSmolModel } from "./model-resolver";
11
+
12
+ const TITLE_SYSTEM_PROMPT = titleSystemPrompt;
13
+
14
+ const MAX_INPUT_CHARS = 2000;
15
+
16
+ /**
17
+ * Find the best available model for title generation.
18
+ * Uses the configured smol model if set, otherwise auto-discovers using priority chain.
19
+ *
20
+ * @param registry Model registry
21
+ * @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
22
+ */
23
+ export async function findTitleModel(registry: ModelRegistry, savedSmolModel?: string): Promise<Model<any> | null> {
24
+ const model = await findSmolModel(registry, savedSmolModel);
25
+ return model ?? null;
26
+ }
27
+
28
+ /**
29
+ * Generate a title for a session based on the first user message.
30
+ *
31
+ * @param firstMessage The first user message
32
+ * @param registry Model registry
33
+ * @param savedSmolModel Optional saved smol model from settings (provider/modelId format)
34
+ */
35
+ export async function generateSessionTitle(
36
+ firstMessage: string,
37
+ registry: ModelRegistry,
38
+ savedSmolModel?: string,
39
+ ): Promise<string | null> {
40
+ const model = await findTitleModel(registry, savedSmolModel);
41
+ if (!model) {
42
+ logger.debug("title-generator: no smol model found");
43
+ return null;
44
+ }
45
+
46
+ const apiKey = await registry.getApiKey(model);
47
+ if (!apiKey) {
48
+ logger.debug("title-generator: no API key for model", { provider: model.provider, id: model.id });
49
+ return null;
50
+ }
51
+
52
+ // Truncate message if too long
53
+ const truncatedMessage =
54
+ firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
55
+
56
+ const request = {
57
+ model: `${model.provider}/${model.id}`,
58
+ systemPrompt: TITLE_SYSTEM_PROMPT,
59
+ userMessage: `<user-message>\n${truncatedMessage}\n</user-message>`,
60
+ maxTokens: 30,
61
+ };
62
+ logger.debug("title-generator: request", request);
63
+
64
+ try {
65
+ const response = await completeSimple(
66
+ model,
67
+ {
68
+ systemPrompt: request.systemPrompt,
69
+ messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
70
+ },
71
+ {
72
+ apiKey,
73
+ maxTokens: 30,
74
+ },
75
+ );
76
+
77
+ // Extract title from response text content
78
+ let title = "";
79
+ for (const content of response.content) {
80
+ if (content.type === "text") {
81
+ title += content.text;
82
+ }
83
+ }
84
+ title = title.trim();
85
+
86
+ logger.debug("title-generator: response", {
87
+ title,
88
+ usage: response.usage,
89
+ stopReason: response.stopReason,
90
+ });
91
+
92
+ if (!title) {
93
+ return null;
94
+ }
95
+
96
+ // Clean up: remove quotes, trailing punctuation
97
+ return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
98
+ } catch (err) {
99
+ logger.debug("title-generator: error", { error: err instanceof Error ? err.message : String(err) });
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Set the terminal title using ANSI escape sequences.
106
+ */
107
+ export function setTerminalTitle(title: string): void {
108
+ // OSC 2 sets the window title
109
+ process.stdout.write(`\x1b]2;${title}\x07`);
110
+ }