@oh-my-pi/pi-coding-agent 1.337.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 (224) hide show
  1. package/CHANGELOG.md +1228 -0
  2. package/README.md +1041 -0
  3. package/docs/compaction.md +403 -0
  4. package/docs/custom-tools.md +541 -0
  5. package/docs/extension-loading.md +1004 -0
  6. package/docs/hooks.md +867 -0
  7. package/docs/rpc.md +1040 -0
  8. package/docs/sdk.md +994 -0
  9. package/docs/session-tree-plan.md +441 -0
  10. package/docs/session.md +240 -0
  11. package/docs/skills.md +290 -0
  12. package/docs/theme.md +637 -0
  13. package/docs/tree.md +197 -0
  14. package/docs/tui.md +341 -0
  15. package/examples/README.md +21 -0
  16. package/examples/custom-tools/README.md +124 -0
  17. package/examples/custom-tools/hello/index.ts +20 -0
  18. package/examples/custom-tools/question/index.ts +84 -0
  19. package/examples/custom-tools/subagent/README.md +172 -0
  20. package/examples/custom-tools/subagent/agents/planner.md +37 -0
  21. package/examples/custom-tools/subagent/agents/reviewer.md +35 -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 +81 -0
  57. package/src/cli/args.ts +246 -0
  58. package/src/cli/file-processor.ts +72 -0
  59. package/src/cli/list-models.ts +104 -0
  60. package/src/cli/plugin-cli.ts +650 -0
  61. package/src/cli/session-picker.ts +41 -0
  62. package/src/cli.ts +10 -0
  63. package/src/commands/init.md +20 -0
  64. package/src/config.ts +159 -0
  65. package/src/core/agent-session.ts +1900 -0
  66. package/src/core/auth-storage.ts +236 -0
  67. package/src/core/bash-executor.ts +196 -0
  68. package/src/core/compaction/branch-summarization.ts +343 -0
  69. package/src/core/compaction/compaction.ts +742 -0
  70. package/src/core/compaction/index.ts +7 -0
  71. package/src/core/compaction/utils.ts +154 -0
  72. package/src/core/custom-tools/index.ts +21 -0
  73. package/src/core/custom-tools/loader.ts +248 -0
  74. package/src/core/custom-tools/types.ts +169 -0
  75. package/src/core/custom-tools/wrapper.ts +28 -0
  76. package/src/core/exec.ts +129 -0
  77. package/src/core/export-html/index.ts +211 -0
  78. package/src/core/export-html/template.css +781 -0
  79. package/src/core/export-html/template.html +54 -0
  80. package/src/core/export-html/template.js +1185 -0
  81. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  82. package/src/core/export-html/vendor/marked.min.js +6 -0
  83. package/src/core/hooks/index.ts +16 -0
  84. package/src/core/hooks/loader.ts +312 -0
  85. package/src/core/hooks/runner.ts +434 -0
  86. package/src/core/hooks/tool-wrapper.ts +99 -0
  87. package/src/core/hooks/types.ts +773 -0
  88. package/src/core/index.ts +52 -0
  89. package/src/core/mcp/client.ts +158 -0
  90. package/src/core/mcp/config.ts +154 -0
  91. package/src/core/mcp/index.ts +45 -0
  92. package/src/core/mcp/loader.ts +68 -0
  93. package/src/core/mcp/manager.ts +181 -0
  94. package/src/core/mcp/tool-bridge.ts +148 -0
  95. package/src/core/mcp/transports/http.ts +316 -0
  96. package/src/core/mcp/transports/index.ts +6 -0
  97. package/src/core/mcp/transports/stdio.ts +252 -0
  98. package/src/core/mcp/types.ts +220 -0
  99. package/src/core/messages.ts +189 -0
  100. package/src/core/model-registry.ts +317 -0
  101. package/src/core/model-resolver.ts +393 -0
  102. package/src/core/plugins/doctor.ts +59 -0
  103. package/src/core/plugins/index.ts +38 -0
  104. package/src/core/plugins/installer.ts +189 -0
  105. package/src/core/plugins/loader.ts +338 -0
  106. package/src/core/plugins/manager.ts +672 -0
  107. package/src/core/plugins/parser.ts +105 -0
  108. package/src/core/plugins/paths.ts +32 -0
  109. package/src/core/plugins/types.ts +190 -0
  110. package/src/core/sdk.ts +760 -0
  111. package/src/core/session-manager.ts +1128 -0
  112. package/src/core/settings-manager.ts +443 -0
  113. package/src/core/skills.ts +437 -0
  114. package/src/core/slash-commands.ts +248 -0
  115. package/src/core/system-prompt.ts +439 -0
  116. package/src/core/timings.ts +25 -0
  117. package/src/core/tools/ask.ts +211 -0
  118. package/src/core/tools/bash-interceptor.ts +120 -0
  119. package/src/core/tools/bash.ts +250 -0
  120. package/src/core/tools/context.ts +32 -0
  121. package/src/core/tools/edit-diff.ts +475 -0
  122. package/src/core/tools/edit.ts +208 -0
  123. package/src/core/tools/exa/company.ts +59 -0
  124. package/src/core/tools/exa/index.ts +64 -0
  125. package/src/core/tools/exa/linkedin.ts +59 -0
  126. package/src/core/tools/exa/logger.ts +56 -0
  127. package/src/core/tools/exa/mcp-client.ts +368 -0
  128. package/src/core/tools/exa/render.ts +196 -0
  129. package/src/core/tools/exa/researcher.ts +90 -0
  130. package/src/core/tools/exa/search.ts +337 -0
  131. package/src/core/tools/exa/types.ts +168 -0
  132. package/src/core/tools/exa/websets.ts +248 -0
  133. package/src/core/tools/find.ts +261 -0
  134. package/src/core/tools/grep.ts +555 -0
  135. package/src/core/tools/index.ts +202 -0
  136. package/src/core/tools/ls.ts +140 -0
  137. package/src/core/tools/lsp/client.ts +605 -0
  138. package/src/core/tools/lsp/config.ts +147 -0
  139. package/src/core/tools/lsp/edits.ts +101 -0
  140. package/src/core/tools/lsp/index.ts +804 -0
  141. package/src/core/tools/lsp/render.ts +447 -0
  142. package/src/core/tools/lsp/rust-analyzer.ts +145 -0
  143. package/src/core/tools/lsp/types.ts +463 -0
  144. package/src/core/tools/lsp/utils.ts +486 -0
  145. package/src/core/tools/notebook.ts +229 -0
  146. package/src/core/tools/path-utils.ts +61 -0
  147. package/src/core/tools/read.ts +240 -0
  148. package/src/core/tools/renderers.ts +540 -0
  149. package/src/core/tools/task/agents.ts +153 -0
  150. package/src/core/tools/task/artifacts.ts +114 -0
  151. package/src/core/tools/task/bundled-agents/browser.md +71 -0
  152. package/src/core/tools/task/bundled-agents/explore.md +82 -0
  153. package/src/core/tools/task/bundled-agents/plan.md +54 -0
  154. package/src/core/tools/task/bundled-agents/reviewer.md +59 -0
  155. package/src/core/tools/task/bundled-agents/task.md +53 -0
  156. package/src/core/tools/task/bundled-commands/architect-plan.md +10 -0
  157. package/src/core/tools/task/bundled-commands/implement-with-critic.md +11 -0
  158. package/src/core/tools/task/bundled-commands/implement.md +11 -0
  159. package/src/core/tools/task/commands.ts +213 -0
  160. package/src/core/tools/task/discovery.ts +208 -0
  161. package/src/core/tools/task/executor.ts +367 -0
  162. package/src/core/tools/task/index.ts +388 -0
  163. package/src/core/tools/task/model-resolver.ts +115 -0
  164. package/src/core/tools/task/parallel.ts +38 -0
  165. package/src/core/tools/task/render.ts +232 -0
  166. package/src/core/tools/task/types.ts +99 -0
  167. package/src/core/tools/truncate.ts +265 -0
  168. package/src/core/tools/web-fetch.ts +2370 -0
  169. package/src/core/tools/web-search/auth.ts +193 -0
  170. package/src/core/tools/web-search/index.ts +537 -0
  171. package/src/core/tools/web-search/providers/anthropic.ts +198 -0
  172. package/src/core/tools/web-search/providers/exa.ts +302 -0
  173. package/src/core/tools/web-search/providers/perplexity.ts +195 -0
  174. package/src/core/tools/web-search/render.ts +182 -0
  175. package/src/core/tools/web-search/types.ts +180 -0
  176. package/src/core/tools/write.ts +99 -0
  177. package/src/index.ts +176 -0
  178. package/src/main.ts +464 -0
  179. package/src/migrations.ts +135 -0
  180. package/src/modes/index.ts +43 -0
  181. package/src/modes/interactive/components/armin.ts +382 -0
  182. package/src/modes/interactive/components/assistant-message.ts +86 -0
  183. package/src/modes/interactive/components/bash-execution.ts +196 -0
  184. package/src/modes/interactive/components/bordered-loader.ts +41 -0
  185. package/src/modes/interactive/components/branch-summary-message.ts +42 -0
  186. package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
  187. package/src/modes/interactive/components/custom-editor.ts +122 -0
  188. package/src/modes/interactive/components/diff.ts +147 -0
  189. package/src/modes/interactive/components/dynamic-border.ts +25 -0
  190. package/src/modes/interactive/components/footer.ts +381 -0
  191. package/src/modes/interactive/components/hook-editor.ts +117 -0
  192. package/src/modes/interactive/components/hook-input.ts +64 -0
  193. package/src/modes/interactive/components/hook-message.ts +96 -0
  194. package/src/modes/interactive/components/hook-selector.ts +91 -0
  195. package/src/modes/interactive/components/model-selector.ts +247 -0
  196. package/src/modes/interactive/components/oauth-selector.ts +120 -0
  197. package/src/modes/interactive/components/plugin-settings.ts +479 -0
  198. package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
  199. package/src/modes/interactive/components/session-selector.ts +204 -0
  200. package/src/modes/interactive/components/settings-selector.ts +453 -0
  201. package/src/modes/interactive/components/show-images-selector.ts +45 -0
  202. package/src/modes/interactive/components/theme-selector.ts +62 -0
  203. package/src/modes/interactive/components/thinking-selector.ts +64 -0
  204. package/src/modes/interactive/components/tool-execution.ts +675 -0
  205. package/src/modes/interactive/components/tree-selector.ts +866 -0
  206. package/src/modes/interactive/components/user-message-selector.ts +159 -0
  207. package/src/modes/interactive/components/user-message.ts +18 -0
  208. package/src/modes/interactive/components/visual-truncate.ts +50 -0
  209. package/src/modes/interactive/components/welcome.ts +183 -0
  210. package/src/modes/interactive/interactive-mode.ts +2516 -0
  211. package/src/modes/interactive/theme/dark.json +101 -0
  212. package/src/modes/interactive/theme/light.json +98 -0
  213. package/src/modes/interactive/theme/theme-schema.json +308 -0
  214. package/src/modes/interactive/theme/theme.ts +998 -0
  215. package/src/modes/print-mode.ts +128 -0
  216. package/src/modes/rpc/rpc-client.ts +527 -0
  217. package/src/modes/rpc/rpc-mode.ts +483 -0
  218. package/src/modes/rpc/rpc-types.ts +203 -0
  219. package/src/utils/changelog.ts +99 -0
  220. package/src/utils/clipboard.ts +265 -0
  221. package/src/utils/fuzzy.ts +108 -0
  222. package/src/utils/mime.ts +30 -0
  223. package/src/utils/shell.ts +276 -0
  224. package/src/utils/tools-manager.ts +274 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * System prompt construction and project context loading
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import { existsSync, readFileSync } from "fs";
7
+ import { join, resolve } from "path";
8
+ import { getAgentDir, getDocsPath, getExamplesPath, getReadmePath } from "../config.js";
9
+ import type { SkillsSettings } from "./settings-manager.js";
10
+ import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills.js";
11
+ import type { ToolName } from "./tools/index.js";
12
+
13
+ /**
14
+ * Execute a git command synchronously and return stdout or null on failure.
15
+ */
16
+ function execGit(args: string[], cwd: string): string | null {
17
+ const result = Bun.spawnSync(["git", ...args], { cwd, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
18
+ if (result.exitCode !== 0) return null;
19
+ return result.stdout.toString().trim() || null;
20
+ }
21
+
22
+ /**
23
+ * Load git context for the system prompt.
24
+ * Returns formatted git status or null if not in a git repo.
25
+ */
26
+ export function loadGitContext(cwd: string): string | null {
27
+ // Check if inside a git repo
28
+ const isGitRepo = execGit(["rev-parse", "--is-inside-work-tree"], cwd);
29
+ if (isGitRepo !== "true") return null;
30
+
31
+ // Get current branch
32
+ const currentBranch = execGit(["rev-parse", "--abbrev-ref", "HEAD"], cwd);
33
+ if (!currentBranch) return null;
34
+
35
+ // Detect main branch (check for 'main' first, then 'master')
36
+ let mainBranch = "main";
37
+ const mainExists = execGit(["rev-parse", "--verify", "main"], cwd);
38
+ if (mainExists === null) {
39
+ const masterExists = execGit(["rev-parse", "--verify", "master"], cwd);
40
+ if (masterExists !== null) mainBranch = "master";
41
+ }
42
+
43
+ // Get git status (porcelain format for parsing)
44
+ const gitStatus = execGit(["status", "--porcelain"], cwd);
45
+ const statusText = gitStatus?.trim() || "(clean)";
46
+
47
+ // Get recent commits
48
+ const recentCommits = execGit(["log", "--oneline", "-5"], cwd);
49
+ const commitsText = recentCommits?.trim() || "(no commits)";
50
+
51
+ 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.
52
+ Current branch: ${currentBranch}
53
+
54
+ Main branch (you will usually use this for PRs): ${mainBranch}
55
+
56
+ Status:
57
+ ${statusText}
58
+
59
+ Recent commits:
60
+ ${commitsText}`;
61
+ }
62
+
63
+ /** Tool descriptions for system prompt */
64
+ const toolDescriptions: Record<ToolName, string> = {
65
+ ask: "Ask user for input or clarification",
66
+ read: "Read file contents",
67
+ bash: "Execute bash commands (git, npm, docker, etc.)",
68
+ edit: "Make surgical edits to files (find exact text and replace)",
69
+ write: "Create or overwrite files",
70
+ grep: "Search file contents for patterns (respects .gitignore)",
71
+ find: "Find files by glob pattern (respects .gitignore)",
72
+ ls: "List directory contents",
73
+ 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.",
74
+ notebook: "Edit Jupyter notebook cells",
75
+ task: "Spawn a sub-agent to handle complex tasks",
76
+ web_fetch: "Fetch and render URLs into clean text for LLM consumption",
77
+ web_search: "Search the web for information",
78
+ };
79
+
80
+ /**
81
+ * Generate anti-bash rules section if the agent has both bash and specialized tools.
82
+ * Only include rules for tools that are actually available.
83
+ */
84
+ function generateAntiBashRules(tools: ToolName[]): string | null {
85
+ const hasBash = tools.includes("bash");
86
+ if (!hasBash) return null;
87
+
88
+ const hasRead = tools.includes("read");
89
+ const hasGrep = tools.includes("grep");
90
+ const hasFind = tools.includes("find");
91
+ const hasLs = tools.includes("ls");
92
+ const hasEdit = tools.includes("edit");
93
+ const hasLsp = tools.includes("lsp");
94
+
95
+ // Only show rules if we have specialized tools that should be preferred
96
+ const hasSpecializedTools = hasRead || hasGrep || hasFind || hasLs || hasEdit;
97
+ if (!hasSpecializedTools) return null;
98
+
99
+ const lines: string[] = [];
100
+ lines.push("## Tool Usage Rules — MANDATORY\n");
101
+ lines.push("### Forbidden Bash Patterns");
102
+ lines.push("NEVER use bash for these operations:\n");
103
+
104
+ if (hasRead) lines.push("- **File reading**: Use `read` instead of cat/head/tail/less/more");
105
+ if (hasGrep) lines.push("- **Content search**: Use `grep` instead of grep/rg/ag/ack");
106
+ if (hasFind) lines.push("- **File finding**: Use `find` instead of find/fd/locate");
107
+ if (hasLs) lines.push("- **Directory listing**: Use `ls` instead of bash ls");
108
+ if (hasEdit) lines.push("- **File editing**: Use `edit` instead of sed/awk/perl -pi/echo >/cat <<EOF");
109
+
110
+ lines.push("\n### Tool Preference (highest → lowest priority)");
111
+ const ladder: string[] = [];
112
+ if (hasLsp) ladder.push("lsp (go-to-definition, references, type info) — DETERMINISTIC");
113
+ if (hasGrep) ladder.push("grep (text/regex search)");
114
+ if (hasFind) ladder.push("find (locate files by pattern)");
115
+ if (hasRead) ladder.push("read (view file contents)");
116
+ if (hasEdit) ladder.push("edit (precise text replacement)");
117
+ ladder.push("bash (ONLY for git, npm, docker, make, cargo, etc.)");
118
+ lines.push(ladder.map((t, i) => `${i + 1}. ${t}`).join("\n"));
119
+
120
+ // Add LSP guidance if available
121
+ if (hasLsp) {
122
+ lines.push("\n### LSP — Preferred for Semantic Queries");
123
+ lines.push("Use `lsp` instead of grep/bash when you need:");
124
+ lines.push("- **Where is X defined?** → `lsp definition`");
125
+ lines.push("- **What calls X?** → `lsp incoming_calls`");
126
+ lines.push("- **What does X call?** → `lsp outgoing_calls`");
127
+ lines.push("- **What type is X?** → `lsp hover`");
128
+ lines.push("- **What symbols are in this file?** → `lsp symbols`");
129
+ lines.push("- **Find symbol across codebase** → `lsp workspace_symbols`\n");
130
+ }
131
+
132
+ // Add search-first protocol
133
+ if (hasGrep || hasFind) {
134
+ lines.push("\n### Search-First Protocol");
135
+ lines.push("Before reading any file:");
136
+ if (hasFind) lines.push("1. Unknown structure → `find` to see file layout");
137
+ if (hasGrep) lines.push("2. Known location → `grep` for specific symbol/error");
138
+ if (hasRead) lines.push("3. Use `read offset/limit` for line ranges, not entire large files");
139
+ lines.push("4. Never read a large file hoping to find something — search first");
140
+ }
141
+
142
+ return lines.join("\n");
143
+ }
144
+
145
+ /** Resolve input as file path or literal string */
146
+ export function resolvePromptInput(input: string | undefined, description: string): string | undefined {
147
+ if (!input) {
148
+ return undefined;
149
+ }
150
+
151
+ if (existsSync(input)) {
152
+ try {
153
+ return readFileSync(input, "utf-8");
154
+ } catch (error) {
155
+ console.error(chalk.yellow(`Warning: Could not read ${description} file ${input}: ${error}`));
156
+ return input;
157
+ }
158
+ }
159
+
160
+ return input;
161
+ }
162
+
163
+ /** Look for AGENTS.md or CLAUDE.md in a directory (prefers AGENTS.md) */
164
+ function loadContextFileFromDir(dir: string): { path: string; content: string } | null {
165
+ const candidates = ["AGENTS.md", "CLAUDE.md"];
166
+ for (const filename of candidates) {
167
+ const filePath = join(dir, filename);
168
+ if (existsSync(filePath)) {
169
+ try {
170
+ return {
171
+ path: filePath,
172
+ content: readFileSync(filePath, "utf-8"),
173
+ };
174
+ } catch (error) {
175
+ console.error(chalk.yellow(`Warning: Could not read ${filePath}: ${error}`));
176
+ }
177
+ }
178
+ }
179
+ return null;
180
+ }
181
+
182
+ export interface LoadContextFilesOptions {
183
+ /** Working directory to start walking up from. Default: process.cwd() */
184
+ cwd?: string;
185
+ /** Agent config directory for global context. Default: from getAgentDir() */
186
+ agentDir?: string;
187
+ }
188
+
189
+ /**
190
+ * Load all project context files in order:
191
+ * 1. Global: agentDir/AGENTS.md or CLAUDE.md
192
+ * 2. Parent directories (top-most first) down to cwd
193
+ * Each returns {path, content} for separate messages
194
+ */
195
+ export function loadProjectContextFiles(
196
+ options: LoadContextFilesOptions = {},
197
+ ): Array<{ path: string; content: string }> {
198
+ const resolvedCwd = options.cwd ?? process.cwd();
199
+ const resolvedAgentDir = options.agentDir ?? getAgentDir();
200
+
201
+ const contextFiles: Array<{ path: string; content: string }> = [];
202
+ const seenPaths = new Set<string>();
203
+
204
+ // 1. Load global context from agentDir
205
+ const globalContext = loadContextFileFromDir(resolvedAgentDir);
206
+ if (globalContext) {
207
+ contextFiles.push(globalContext);
208
+ seenPaths.add(globalContext.path);
209
+ }
210
+
211
+ // 2. Walk up from cwd to root, collecting all context files
212
+ const ancestorContextFiles: Array<{ path: string; content: string }> = [];
213
+
214
+ let currentDir = resolvedCwd;
215
+ const root = resolve("/");
216
+
217
+ while (true) {
218
+ const contextFile = loadContextFileFromDir(currentDir);
219
+ if (contextFile && !seenPaths.has(contextFile.path)) {
220
+ // Add to beginning so we get top-most parent first
221
+ ancestorContextFiles.unshift(contextFile);
222
+ seenPaths.add(contextFile.path);
223
+ }
224
+
225
+ // Stop if we've reached root
226
+ if (currentDir === root) break;
227
+
228
+ // Move up one directory
229
+ const parentDir = resolve(currentDir, "..");
230
+ if (parentDir === currentDir) break; // Safety check
231
+ currentDir = parentDir;
232
+ }
233
+
234
+ // Add ancestor files in order (top-most → cwd)
235
+ contextFiles.push(...ancestorContextFiles);
236
+
237
+ return contextFiles;
238
+ }
239
+
240
+ export interface BuildSystemPromptOptions {
241
+ /** Custom system prompt (replaces default). */
242
+ customPrompt?: string;
243
+ /** Tools to include in prompt. Default: [read, bash, edit, write] */
244
+ selectedTools?: ToolName[];
245
+ /** Text to append to system prompt. */
246
+ appendSystemPrompt?: string;
247
+ /** Skills settings for discovery. */
248
+ skillsSettings?: SkillsSettings;
249
+ /** Working directory. Default: process.cwd() */
250
+ cwd?: string;
251
+ /** Agent config directory. Default: from getAgentDir() */
252
+ agentDir?: string;
253
+ /** Pre-loaded context files (skips discovery if provided). */
254
+ contextFiles?: Array<{ path: string; content: string }>;
255
+ /** Pre-loaded skills (skips discovery if provided). */
256
+ skills?: Skill[];
257
+ }
258
+
259
+ /** Build the system prompt with tools, guidelines, and context */
260
+ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): string {
261
+ const {
262
+ customPrompt,
263
+ selectedTools,
264
+ appendSystemPrompt,
265
+ skillsSettings,
266
+ cwd,
267
+ agentDir,
268
+ contextFiles: providedContextFiles,
269
+ skills: providedSkills,
270
+ } = options;
271
+ const resolvedCwd = cwd ?? process.cwd();
272
+ const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
273
+ const resolvedAppendPrompt = resolvePromptInput(appendSystemPrompt, "append system prompt");
274
+
275
+ const now = new Date();
276
+ const dateTime = now.toLocaleString("en-US", {
277
+ weekday: "long",
278
+ year: "numeric",
279
+ month: "long",
280
+ day: "numeric",
281
+ hour: "2-digit",
282
+ minute: "2-digit",
283
+ second: "2-digit",
284
+ timeZoneName: "short",
285
+ });
286
+
287
+ const appendSection = resolvedAppendPrompt ? `\n\n${resolvedAppendPrompt}` : "";
288
+
289
+ // Resolve context files: use provided or discover
290
+ const contextFiles = providedContextFiles ?? loadProjectContextFiles({ cwd: resolvedCwd, agentDir });
291
+
292
+ // Resolve skills: use provided or discover
293
+ const skills =
294
+ providedSkills ??
295
+ (skillsSettings?.enabled !== false ? loadSkills({ ...skillsSettings, cwd: resolvedCwd, agentDir }).skills : []);
296
+
297
+ if (resolvedCustomPrompt) {
298
+ let prompt = resolvedCustomPrompt;
299
+
300
+ if (appendSection) {
301
+ prompt += appendSection;
302
+ }
303
+
304
+ // Append project context files
305
+ if (contextFiles.length > 0) {
306
+ prompt += "\n\n# Project Context\n\n";
307
+ prompt += "The following project context files have been loaded:\n\n";
308
+ for (const { path: filePath, content } of contextFiles) {
309
+ prompt += `## ${filePath}\n\n${content}\n\n`;
310
+ }
311
+ }
312
+
313
+ // Append git context if in a git repo
314
+ const gitContext = loadGitContext(resolvedCwd);
315
+ if (gitContext) {
316
+ prompt += `\n\n# Git Status\n\n${gitContext}`;
317
+ }
318
+
319
+ // Append skills section (only if read tool is available)
320
+ const customPromptHasRead = !selectedTools || selectedTools.includes("read");
321
+ if (customPromptHasRead && skills.length > 0) {
322
+ prompt += formatSkillsForPrompt(skills);
323
+ }
324
+
325
+ // Add date/time and working directory last
326
+ prompt += `\nCurrent date and time: ${dateTime}`;
327
+ prompt += `\nCurrent working directory: ${resolvedCwd}`;
328
+
329
+ return prompt;
330
+ }
331
+
332
+ // Get absolute paths to documentation and examples
333
+ const readmePath = getReadmePath();
334
+ const docsPath = getDocsPath();
335
+ const examplesPath = getExamplesPath();
336
+
337
+ // Build tools list based on selected tools
338
+ const tools = selectedTools || (["read", "bash", "edit", "write"] as ToolName[]);
339
+ const toolsList = tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n");
340
+
341
+ // Generate anti-bash rules (returns null if not applicable)
342
+ const antiBashSection = generateAntiBashRules(tools);
343
+
344
+ // Build guidelines based on which tools are actually available
345
+ const guidelinesList: string[] = [];
346
+
347
+ const hasBash = tools.includes("bash");
348
+ const hasEdit = tools.includes("edit");
349
+ const hasWrite = tools.includes("write");
350
+ const hasRead = tools.includes("read");
351
+
352
+ // Read-only mode notice (no bash, edit, or write)
353
+ if (!hasBash && !hasEdit && !hasWrite) {
354
+ guidelinesList.push("You are in READ-ONLY mode - you cannot modify files or execute arbitrary commands");
355
+ }
356
+
357
+ // Bash without edit/write = read-only bash mode
358
+ if (hasBash && !hasEdit && !hasWrite) {
359
+ guidelinesList.push(
360
+ "Use bash ONLY for read-only operations (git log, gh issue view, curl, etc.) - do NOT modify any files",
361
+ );
362
+ }
363
+
364
+ // Read before edit guideline
365
+ if (hasRead && hasEdit) {
366
+ guidelinesList.push("Use read to examine files before editing");
367
+ }
368
+
369
+ // Edit guideline
370
+ if (hasEdit) {
371
+ guidelinesList.push(
372
+ "Use edit for precise changes (old text must match exactly, fuzzy matching handles whitespace)",
373
+ );
374
+ }
375
+
376
+ // Write guideline
377
+ if (hasWrite) {
378
+ guidelinesList.push("Use write only for new files or complete rewrites");
379
+ }
380
+
381
+ // Output guideline (only when actually writing/executing)
382
+ if (hasEdit || hasWrite) {
383
+ guidelinesList.push(
384
+ "When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
385
+ );
386
+ }
387
+
388
+ // Always include these
389
+ guidelinesList.push("Be concise in your responses");
390
+ guidelinesList.push("Show file paths clearly when working with files");
391
+
392
+ const guidelines = guidelinesList.map((g) => `- ${g}`).join("\n");
393
+
394
+ // Build the prompt with anti-bash rules prominently placed
395
+ let prompt = `You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
396
+
397
+ Available tools:
398
+ ${toolsList}
399
+ ${antiBashSection ? `\n${antiBashSection}\n` : ""}
400
+ Guidelines:
401
+ ${guidelines}
402
+
403
+ Documentation:
404
+ - Main documentation: ${readmePath}
405
+ - Additional docs: ${docsPath}
406
+ - Examples: ${examplesPath} (hooks, custom tools, SDK)
407
+ - When asked to create: custom models/providers (README.md), hooks (docs/hooks.md, examples/hooks/), custom tools (docs/custom-tools.md, docs/tui.md, examples/custom-tools/), themes (docs/theme.md), skills (docs/skills.md)
408
+ - Always read the doc, examples, AND follow .md cross-references before implementing`;
409
+
410
+ if (appendSection) {
411
+ prompt += appendSection;
412
+ }
413
+
414
+ // Append project context files
415
+ if (contextFiles.length > 0) {
416
+ prompt += "\n\n# Project Context\n\n";
417
+ prompt += "The following project context files have been loaded:\n\n";
418
+ for (const { path: filePath, content } of contextFiles) {
419
+ prompt += `## ${filePath}\n\n${content}\n\n`;
420
+ }
421
+ }
422
+
423
+ // Append git context if in a git repo
424
+ const gitContext = loadGitContext(resolvedCwd);
425
+ if (gitContext) {
426
+ prompt += `\n\n# Git Status\n\n${gitContext}`;
427
+ }
428
+
429
+ // Append skills section (only if read tool is available)
430
+ if (hasRead && skills.length > 0) {
431
+ prompt += formatSkillsForPrompt(skills);
432
+ }
433
+
434
+ // Add date/time and working directory last
435
+ prompt += `\nCurrent date and time: ${dateTime}`;
436
+ prompt += `\nCurrent working directory: ${resolvedCwd}`;
437
+
438
+ return prompt;
439
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Central timing instrumentation for startup profiling.
3
+ * Enable with PI_TIMING=1 environment variable.
4
+ */
5
+
6
+ const ENABLED = 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,211 @@
1
+ /**
2
+ * Ask Tool - Interactive user prompting during execution
3
+ *
4
+ * Use this tool when you need to ask the user questions during execution.
5
+ * This allows you to:
6
+ * 1. Gather user preferences or requirements
7
+ * 2. Clarify ambiguous instructions
8
+ * 3. Get decisions on implementation choices as you work
9
+ * 4. Offer choices to the user about what direction to take
10
+ *
11
+ * Usage notes:
12
+ * - Users will always be able to select "Other" to provide custom text input
13
+ * - Use multi: true to allow multiple answers to be selected for a question
14
+ * - If you recommend a specific option, make that the first option in the list
15
+ * and add "(Recommended)" at the end of the label
16
+ */
17
+
18
+ import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
+ import { Type } from "@sinclair/typebox";
20
+
21
+ // =============================================================================
22
+ // Types
23
+ // =============================================================================
24
+
25
+ const OptionItem = Type.Object({
26
+ label: Type.String({ description: "Display label for this option" }),
27
+ });
28
+
29
+ const askSchema = Type.Object({
30
+ question: Type.String({ description: "The question to ask the user" }),
31
+ options: Type.Array(OptionItem, {
32
+ description: "Available options for the user to choose from.",
33
+ minItems: 1,
34
+ }),
35
+ multi: Type.Optional(
36
+ Type.Boolean({
37
+ description: "Allow multiple options to be selected (default: false)",
38
+ default: false,
39
+ }),
40
+ ),
41
+ });
42
+
43
+ export interface AskToolDetails {
44
+ question: string;
45
+ options: string[];
46
+ multi: boolean;
47
+ selectedOptions: string[];
48
+ customInput?: string;
49
+ }
50
+
51
+ // =============================================================================
52
+ // Constants
53
+ // =============================================================================
54
+
55
+ const OTHER_OPTION = "Other (type your own)";
56
+ const DONE_OPTION = "✓ Done selecting";
57
+
58
+ const DESCRIPTION = `Use this tool when you need to ask the user questions during execution. This allows you to:
59
+ 1. Gather user preferences or requirements
60
+ 2. Clarify ambiguous instructions
61
+ 3. Get decisions on implementation choices as you work
62
+ 4. Offer choices to the user about what direction to take.
63
+
64
+ Usage notes:
65
+ - Users will always be able to select "Other" to provide custom text input
66
+ - Use multi: true to allow multiple answers to be selected for a question
67
+ - If you recommend a specific option, make that the first option in the list and add "(Recommended)" at the end of the label
68
+
69
+ Example usage:
70
+
71
+ <example>
72
+ assistant: Let me ask which features you want to include.
73
+ assistant: Uses the ask tool:
74
+ {
75
+ "question": "Which features should I implement?",
76
+ "options": [
77
+ {"label": "Authentication"},
78
+ {"label": "API endpoints"},
79
+ {"label": "Database models"},
80
+ {"label": "Unit tests"},
81
+ {"label": "Documentation"}
82
+ ],
83
+ "multi": true
84
+ }
85
+ </example>`;
86
+
87
+ // =============================================================================
88
+ // Tool Implementation
89
+ // =============================================================================
90
+
91
+ export function createAskTool(_cwd: string): AgentTool<typeof askSchema, AskToolDetails> {
92
+ return {
93
+ name: "ask",
94
+ label: "Ask",
95
+ description: DESCRIPTION,
96
+ parameters: askSchema,
97
+
98
+ async execute(
99
+ _toolCallId: string,
100
+ params: { question: string; options: Array<{ label: string }>; multi?: boolean },
101
+ _signal?: AbortSignal,
102
+ _onUpdate?: AgentToolUpdateCallback<AskToolDetails>,
103
+ context?: AgentToolContext,
104
+ ) {
105
+ const { question, options, multi = false } = params;
106
+ const optionLabels = options.map((o) => o.label);
107
+
108
+ // Headless fallback - return error if no UI available
109
+ if (!context?.hasUI || !context.ui) {
110
+ return {
111
+ content: [
112
+ {
113
+ type: "text" as const,
114
+ text: "Error: User prompt requires interactive mode",
115
+ },
116
+ ],
117
+ details: {
118
+ question,
119
+ options: optionLabels,
120
+ multi,
121
+ selectedOptions: [],
122
+ },
123
+ };
124
+ }
125
+
126
+ const { ui } = context;
127
+ let selectedOptions: string[] = [];
128
+ let customInput: string | undefined;
129
+
130
+ if (multi) {
131
+ // Multi-select: show checkboxes in the label to indicate selection state
132
+ const selected = new Set<string>();
133
+
134
+ while (true) {
135
+ // Build options with checkbox indicators
136
+ const opts: string[] = [];
137
+
138
+ // Add "Done" option if any selected
139
+ if (selected.size > 0) {
140
+ opts.push(DONE_OPTION);
141
+ }
142
+
143
+ // Add all options with [X] or [ ] prefix
144
+ for (const opt of optionLabels) {
145
+ const checkbox = selected.has(opt) ? "[X]" : "[ ]";
146
+ opts.push(`${checkbox} ${opt}`);
147
+ }
148
+
149
+ // Add "Other" option
150
+ opts.push(OTHER_OPTION);
151
+
152
+ const prefix = selected.size > 0 ? `(${selected.size} selected) ` : "";
153
+ const choice = await ui.select(`${prefix}${question}`, opts);
154
+
155
+ if (choice === undefined || choice === DONE_OPTION) break;
156
+
157
+ if (choice === OTHER_OPTION) {
158
+ const input = await ui.input("Enter your response:");
159
+ if (input) customInput = input;
160
+ break;
161
+ }
162
+
163
+ // Toggle selection - extract the actual option name
164
+ const optMatch = choice.match(/^\[.\] (.+)$/);
165
+ if (optMatch) {
166
+ const opt = optMatch[1];
167
+ if (selected.has(opt)) {
168
+ selected.delete(opt);
169
+ } else {
170
+ selected.add(opt);
171
+ }
172
+ }
173
+ }
174
+ selectedOptions = Array.from(selected);
175
+ } else {
176
+ // Single select with "Other" option
177
+ const choice = await ui.select(question, [...optionLabels, OTHER_OPTION]);
178
+ if (choice === OTHER_OPTION) {
179
+ const input = await ui.input("Enter your response:");
180
+ if (input) customInput = input;
181
+ } else if (choice) {
182
+ selectedOptions = [choice];
183
+ }
184
+ }
185
+
186
+ const details: AskToolDetails = {
187
+ question,
188
+ options: optionLabels,
189
+ multi,
190
+ selectedOptions,
191
+ customInput,
192
+ };
193
+
194
+ let responseText: string;
195
+ if (customInput) {
196
+ responseText = `User provided custom input: ${customInput}`;
197
+ } else if (selectedOptions.length > 0) {
198
+ responseText = multi
199
+ ? `User selected: ${selectedOptions.join(", ")}`
200
+ : `User selected: ${selectedOptions[0]}`;
201
+ } else {
202
+ responseText = "User cancelled the selection";
203
+ }
204
+
205
+ return { content: [{ type: "text" as const, text: responseText }], details };
206
+ },
207
+ };
208
+ }
209
+
210
+ /** Default ask tool using process.cwd() - for backwards compatibility (no UI) */
211
+ export const askTool = createAskTool(process.cwd());