@oh-my-pi/pi-coding-agent 4.0.1 → 4.2.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 (85) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +2 -1
  3. package/docs/sdk.md +0 -3
  4. package/package.json +6 -5
  5. package/src/config.ts +9 -0
  6. package/src/core/agent-storage.ts +450 -0
  7. package/src/core/auth-storage.ts +111 -184
  8. package/src/core/compaction/branch-summarization.ts +5 -4
  9. package/src/core/compaction/compaction.ts +7 -6
  10. package/src/core/compaction/utils.ts +6 -11
  11. package/src/core/custom-commands/bundled/review/index.ts +22 -94
  12. package/src/core/custom-share.ts +66 -0
  13. package/src/core/history-storage.ts +174 -0
  14. package/src/core/index.ts +1 -0
  15. package/src/core/keybindings.ts +3 -0
  16. package/src/core/prompt-templates.ts +271 -1
  17. package/src/core/sdk.ts +14 -3
  18. package/src/core/settings-manager.ts +100 -34
  19. package/src/core/slash-commands.ts +4 -1
  20. package/src/core/storage-migration.ts +215 -0
  21. package/src/core/system-prompt.ts +87 -289
  22. package/src/core/title-generator.ts +3 -2
  23. package/src/core/tools/ask.ts +2 -2
  24. package/src/core/tools/bash.ts +2 -1
  25. package/src/core/tools/calculator.ts +2 -1
  26. package/src/core/tools/edit.ts +2 -1
  27. package/src/core/tools/find.ts +2 -1
  28. package/src/core/tools/gemini-image.ts +2 -1
  29. package/src/core/tools/git.ts +2 -2
  30. package/src/core/tools/grep.ts +2 -1
  31. package/src/core/tools/index.test.ts +0 -28
  32. package/src/core/tools/index.ts +0 -6
  33. package/src/core/tools/lsp/index.ts +2 -1
  34. package/src/core/tools/output.ts +2 -1
  35. package/src/core/tools/read.ts +4 -1
  36. package/src/core/tools/ssh.ts +4 -2
  37. package/src/core/tools/task/agents.ts +56 -30
  38. package/src/core/tools/task/commands.ts +9 -8
  39. package/src/core/tools/task/index.ts +7 -15
  40. package/src/core/tools/web-fetch.ts +2 -1
  41. package/src/core/tools/web-search/auth.ts +106 -16
  42. package/src/core/tools/web-search/index.ts +3 -2
  43. package/src/core/tools/web-search/providers/anthropic.ts +44 -6
  44. package/src/core/tools/write.ts +2 -1
  45. package/src/core/voice.ts +3 -1
  46. package/src/main.ts +1 -1
  47. package/src/migrations.ts +20 -20
  48. package/src/modes/interactive/components/custom-editor.ts +7 -0
  49. package/src/modes/interactive/components/history-search.ts +158 -0
  50. package/src/modes/interactive/controllers/command-controller.ts +527 -0
  51. package/src/modes/interactive/controllers/event-controller.ts +340 -0
  52. package/src/modes/interactive/controllers/extension-ui-controller.ts +600 -0
  53. package/src/modes/interactive/controllers/input-controller.ts +585 -0
  54. package/src/modes/interactive/controllers/selector-controller.ts +585 -0
  55. package/src/modes/interactive/interactive-mode.ts +370 -3115
  56. package/src/modes/interactive/theme/theme.ts +5 -5
  57. package/src/modes/interactive/types.ts +189 -0
  58. package/src/modes/interactive/utils/ui-helpers.ts +449 -0
  59. package/src/modes/interactive/utils/voice-manager.ts +96 -0
  60. package/src/prompts/{explore.md → agents/explore.md} +7 -5
  61. package/src/prompts/agents/frontmatter.md +7 -0
  62. package/src/prompts/{plan.md → agents/plan.md} +3 -3
  63. package/src/prompts/{task.md → agents/task.md} +1 -1
  64. package/src/prompts/review-request.md +44 -8
  65. package/src/prompts/system/custom-system-prompt.md +80 -0
  66. package/src/prompts/system/file-operations.md +12 -0
  67. package/src/prompts/system/system-prompt.md +232 -0
  68. package/src/prompts/system/title-system.md +2 -0
  69. package/src/prompts/tools/bash.md +1 -1
  70. package/src/prompts/tools/read.md +1 -1
  71. package/src/prompts/tools/task.md +9 -3
  72. package/src/core/tools/rulebook.ts +0 -132
  73. package/src/prompts/system-prompt.md +0 -43
  74. package/src/prompts/title-system.md +0 -8
  75. /package/src/prompts/{architect-plan.md → agents/architect-plan.md} +0 -0
  76. /package/src/prompts/{implement-with-critic.md → agents/implement-with-critic.md} +0 -0
  77. /package/src/prompts/{implement.md → agents/implement.md} +0 -0
  78. /package/src/prompts/{init.md → agents/init.md} +0 -0
  79. /package/src/prompts/{reviewer.md → agents/reviewer.md} +0 -0
  80. /package/src/prompts/{branch-summary-preamble.md → compaction/branch-summary-preamble.md} +0 -0
  81. /package/src/prompts/{branch-summary.md → compaction/branch-summary.md} +0 -0
  82. /package/src/prompts/{compaction-summary.md → compaction/compaction-summary.md} +0 -0
  83. /package/src/prompts/{compaction-turn-prefix.md → compaction/compaction-turn-prefix.md} +0 -0
  84. /package/src/prompts/{compaction-update-summary.md → compaction/compaction-update-summary.md} +0 -0
  85. /package/src/prompts/{summarization-system.md → system/summarization-system.md} +0 -0
@@ -14,6 +14,7 @@
14
14
 
15
15
  import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
16
16
  import type { HookCommandContext } from "../../../hooks/types";
17
+ import { renderPromptTemplate } from "../../../prompt-templates";
17
18
  import type { CustomCommand, CustomCommandAPI } from "../../types";
18
19
 
19
20
  // ─────────────────────────────────────────────────────────────────────────────
@@ -164,20 +165,6 @@ function getRecommendedAgentCount(stats: DiffStats): number {
164
165
  return Math.min(16, fileCount);
165
166
  }
166
167
 
167
- /**
168
- * Format diff stats as a markdown table for the prompt.
169
- */
170
- function formatFileTable(files: FileDiff[]): string {
171
- if (files.length === 0) return "_No files to review._";
172
-
173
- const rows = files.map((f) => {
174
- const ext = getFileExt(f.path);
175
- return `| ${f.path} | +${f.linesAdded}/-${f.linesRemoved} | ${ext} |`;
176
- });
177
-
178
- return `| File | +/- | Type |\n|------|-----|------|\n${rows.join("\n")}`;
179
- }
180
-
181
168
  /**
182
169
  * Extract first N lines of actual diff content (excluding headers) for preview.
183
170
  */
@@ -203,33 +190,6 @@ function getDiffPreview(hunks: string, maxLines: number): string {
203
190
  return contentLines.join("\n");
204
191
  }
205
192
 
206
- /**
207
- * Format condensed diff previews for large changesets.
208
- */
209
- function formatDiffPreviews(files: FileDiff[], linesPerFile: number): string {
210
- const parts: string[] = [];
211
-
212
- for (const f of files) {
213
- const preview = getDiffPreview(f.hunks, linesPerFile);
214
- if (preview.trim()) {
215
- parts.push(`#### ${f.path}\n\`\`\`diff\n${preview}\n\`\`\``);
216
- }
217
- }
218
-
219
- return parts.join("\n\n");
220
- }
221
-
222
- /**
223
- * Format excluded files list for the prompt.
224
- */
225
- function formatExcluded(excluded: DiffStats["excluded"]): string {
226
- if (excluded.length === 0) return "";
227
-
228
- const items = excluded.map((e) => `- \`${e.path}\` (+${e.linesAdded}/-${e.linesRemoved}) — ${e.reason}`);
229
-
230
- return `### Excluded Files (${excluded.length})\n\n${items.join("\n")}`;
231
- }
232
-
233
193
  // Thresholds for diff inclusion
234
194
  const MAX_DIFF_CHARS = 50_000; // Don't include diff above this
235
195
  const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than this
@@ -241,59 +201,27 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
241
201
  const agentCount = getRecommendedAgentCount(stats);
242
202
  const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
243
203
  const totalLines = stats.totalAdded + stats.totalRemoved;
244
-
245
- // Build distribution guidance
246
- const distributionGuidance =
247
- `Based on the diff weight (~${totalLines} lines across ${stats.files.length} files), ` +
248
- (agentCount === 1 ? `use **1 reviewer agent**.` : `spawn **${agentCount} reviewer agents** in parallel.`);
249
-
250
- // Build grouping guidance (only for multi-agent)
251
- const groupingGuidance =
252
- agentCount > 1
253
- ? `Group files by locality (related changes together). For example:
254
- - Files in the same directory or module → same agent
255
- - Files that implement related functionality → same agent
256
- - Test files with their implementation files → same agent
257
-
258
- Use the Task tool with \`agent: "reviewer"\` and the batch \`tasks\` array to run reviews in parallel.`
259
- : "";
260
-
261
- // Build diff section
262
- let diffSection: string;
263
- if (!skipDiff) {
264
- diffSection = `### Diff
265
-
266
- <diff>
267
- ${rawDiff.trim()}
268
- </diff>`;
269
- } else {
270
- const linesPerFile = Math.max(5, Math.floor(100 / stats.files.length));
271
- diffSection = `### Diff Previews
272
-
273
- _Full diff too large (${stats.files.length} files). Showing first ~${linesPerFile} lines per file. Reviewers should fetch full diffs for assigned files._
274
-
275
- ${formatDiffPreviews(stats.files, linesPerFile)}`;
276
- }
277
-
278
- // Build diff instruction
279
- const diffInstruction = skipDiff
280
- ? "Run `git diff` or `git show` to get the diff for assigned files"
281
- : "Use the diff hunks provided below (don't re-run git diff)";
282
-
283
- // Replace template variables
284
- return reviewRequestTemplate
285
- .replace("{MODE}", mode)
286
- .replace("{FILE_COUNT}", String(stats.files.length))
287
- .replace("{LINES_ADDED}", String(stats.totalAdded))
288
- .replace("{LINES_REMOVED}", String(stats.totalRemoved))
289
- .replace("{FILE_TABLE}", formatFileTable(stats.files))
290
- .replace("{EXCLUDED_SECTION}", stats.excluded.length > 0 ? formatExcluded(stats.excluded) : "")
291
- .replace("{DISTRIBUTION_GUIDANCE}", distributionGuidance)
292
- .replace("{GROUPING_GUIDANCE}", groupingGuidance)
293
- .replace("{DIFF_INSTRUCTION}", diffInstruction)
294
- .replace("{DIFF_SECTION}", diffSection)
295
- .replace(/\n{3,}/g, "\n\n") // Collapse multiple blank lines
296
- .trim();
204
+ const linesPerFile = skipDiff ? Math.max(5, Math.floor(100 / stats.files.length)) : 0;
205
+
206
+ const filesWithExt = stats.files.map((f) => ({
207
+ ...f,
208
+ ext: getFileExt(f.path),
209
+ hunksPreview: skipDiff ? getDiffPreview(f.hunks, linesPerFile) : "",
210
+ }));
211
+
212
+ return renderPromptTemplate(reviewRequestTemplate, {
213
+ mode,
214
+ files: filesWithExt,
215
+ excluded: stats.excluded,
216
+ totalAdded: stats.totalAdded,
217
+ totalRemoved: stats.totalRemoved,
218
+ totalLines,
219
+ agentCount,
220
+ multiAgent: agentCount > 1,
221
+ skipDiff,
222
+ rawDiff: rawDiff.trim(),
223
+ linesPerFile,
224
+ });
297
225
  }
298
226
 
299
227
  export function createReviewCommand(api: CustomCommandAPI): CustomCommand {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Custom share script loader.
3
+ *
4
+ * Allows users to define a custom share handler at ~/.omp/agent/share.ts
5
+ * that will be used instead of the default GitHub Gist sharing.
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { getAgentDir } from "../config";
11
+
12
+ export interface CustomShareResult {
13
+ /** URL to display/open (optional - script may handle everything itself) */
14
+ url?: string;
15
+ /** Additional message to show the user */
16
+ message?: string;
17
+ }
18
+
19
+ export type CustomShareFn = (htmlPath: string) => Promise<CustomShareResult | string | undefined>;
20
+
21
+ interface LoadedCustomShare {
22
+ path: string;
23
+ fn: CustomShareFn;
24
+ }
25
+
26
+ const SHARE_SCRIPT_CANDIDATES = ["share.ts", "share.js", "share.mjs"];
27
+
28
+ /**
29
+ * Get the path to the custom share script if it exists.
30
+ */
31
+ export function getCustomSharePath(): string | null {
32
+ const agentDir = getAgentDir();
33
+
34
+ for (const candidate of SHARE_SCRIPT_CANDIDATES) {
35
+ const scriptPath = join(agentDir, candidate);
36
+ if (existsSync(scriptPath)) {
37
+ return scriptPath;
38
+ }
39
+ }
40
+
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Load the custom share script if it exists.
46
+ */
47
+ export async function loadCustomShare(): Promise<LoadedCustomShare | null> {
48
+ const scriptPath = getCustomSharePath();
49
+ if (!scriptPath) {
50
+ return null;
51
+ }
52
+
53
+ try {
54
+ const module = await import(scriptPath);
55
+ const fn = module.default;
56
+
57
+ if (typeof fn !== "function") {
58
+ throw new Error("share script must export a default function");
59
+ }
60
+
61
+ return { path: scriptPath, fn };
62
+ } catch (err) {
63
+ const message = err instanceof Error ? err.message : String(err);
64
+ throw new Error(`Failed to load share script: ${message}`);
65
+ }
66
+ }
@@ -0,0 +1,174 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { dirname, join } from "node:path";
3
+ import { getAgentDir } from "../config";
4
+ import { logger } from "./logger";
5
+
6
+ export interface HistoryEntry {
7
+ id: number;
8
+ prompt: string;
9
+ created_at: number;
10
+ cwd?: string;
11
+ }
12
+
13
+ type Statement = ReturnType<Database["prepare"]>;
14
+
15
+ type HistoryRow = {
16
+ id: number;
17
+ prompt: string;
18
+ created_at: number;
19
+ cwd: string | null;
20
+ };
21
+
22
+ export class HistoryStorage {
23
+ private db: Database;
24
+ private static instance?: HistoryStorage;
25
+
26
+ // Prepared statements
27
+ private insertStmt: Statement;
28
+ private recentStmt: Statement;
29
+ private searchStmt: Statement;
30
+ private lastPromptStmt: Statement;
31
+
32
+ // In-memory cache of last prompt to avoid sync DB reads on add
33
+ private lastPromptCache: string | null = null;
34
+
35
+ private constructor(dbPath: string) {
36
+ this.ensureDir(dbPath);
37
+
38
+ this.db = new Database(dbPath);
39
+
40
+ const hasFts = this.db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='history_fts'").get();
41
+
42
+ this.db.exec(`
43
+ PRAGMA journal_mode=WAL;
44
+ PRAGMA synchronous=NORMAL;
45
+
46
+ CREATE TABLE IF NOT EXISTS history (
47
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
48
+ prompt TEXT NOT NULL,
49
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
50
+ cwd TEXT
51
+ );
52
+ CREATE INDEX IF NOT EXISTS idx_history_created_at ON history(created_at DESC);
53
+
54
+ CREATE VIRTUAL TABLE IF NOT EXISTS history_fts USING fts5(prompt, content='history', content_rowid='id');
55
+
56
+ CREATE TRIGGER IF NOT EXISTS history_ai AFTER INSERT ON history BEGIN
57
+ INSERT INTO history_fts(rowid, prompt) VALUES (new.id, new.prompt);
58
+ END;
59
+ `);
60
+
61
+ if (!hasFts) {
62
+ try {
63
+ this.db.run("INSERT INTO history_fts(history_fts) VALUES('rebuild')");
64
+ } catch (error) {
65
+ logger.warn("HistoryStorage FTS rebuild failed", { error: String(error) });
66
+ }
67
+ }
68
+
69
+ this.insertStmt = this.db.prepare("INSERT INTO history (prompt, cwd) VALUES (?, ?)");
70
+ this.recentStmt = this.db.prepare(
71
+ "SELECT id, prompt, created_at, cwd FROM history ORDER BY created_at DESC, id DESC LIMIT ?",
72
+ );
73
+ this.searchStmt = this.db.prepare(
74
+ "SELECT h.id, h.prompt, h.created_at, h.cwd FROM history_fts f JOIN history h ON h.id = f.rowid WHERE history_fts MATCH ? ORDER BY h.created_at DESC, h.id DESC LIMIT ?",
75
+ );
76
+ this.lastPromptStmt = this.db.prepare("SELECT prompt FROM history ORDER BY id DESC LIMIT 1");
77
+
78
+ const last = this.lastPromptStmt.get() as { prompt?: string } | undefined;
79
+ this.lastPromptCache = last?.prompt ?? null;
80
+ }
81
+
82
+ static open(dbPath: string = join(getAgentDir(), "history.db")): HistoryStorage {
83
+ if (!HistoryStorage.instance) {
84
+ HistoryStorage.instance = new HistoryStorage(dbPath);
85
+ }
86
+ return HistoryStorage.instance;
87
+ }
88
+
89
+ add(prompt: string, cwd?: string): void {
90
+ const trimmed = prompt.trim();
91
+ if (!trimmed) return;
92
+ if (this.lastPromptCache === trimmed) return;
93
+
94
+ this.lastPromptCache = trimmed;
95
+
96
+ setImmediate(() => {
97
+ try {
98
+ this.insertStmt.run(trimmed, cwd ?? null);
99
+ } catch (error) {
100
+ logger.error("HistoryStorage add failed", { error: String(error) });
101
+ }
102
+ });
103
+ }
104
+
105
+ getRecent(limit: number): HistoryEntry[] {
106
+ const safeLimit = this.normalizeLimit(limit);
107
+ if (safeLimit === 0) return [];
108
+
109
+ try {
110
+ const rows = this.recentStmt.all(safeLimit) as HistoryRow[];
111
+ return rows.map((row) => this.toEntry(row));
112
+ } catch (error) {
113
+ logger.error("HistoryStorage getRecent failed", { error: String(error) });
114
+ return [];
115
+ }
116
+ }
117
+
118
+ search(query: string, limit: number): HistoryEntry[] {
119
+ const safeLimit = this.normalizeLimit(limit);
120
+ if (safeLimit === 0) return [];
121
+
122
+ const ftsQuery = this.buildFtsQuery(query);
123
+ if (!ftsQuery) return [];
124
+
125
+ try {
126
+ const rows = this.searchStmt.all(ftsQuery, safeLimit) as HistoryRow[];
127
+ return rows.map((row) => this.toEntry(row));
128
+ } catch (error) {
129
+ logger.error("HistoryStorage search failed", { error: String(error) });
130
+ return [];
131
+ }
132
+ }
133
+
134
+ private ensureDir(dbPath: string): void {
135
+ const dir = dirname(dbPath);
136
+ const result = Bun.spawnSync(["mkdir", "-p", dir]);
137
+ if (result.exitCode !== 0) {
138
+ const stderr = result.stderr ? new TextDecoder().decode(result.stderr) : "";
139
+ throw new Error(`Failed to create history directory: ${dir} ${stderr}`.trim());
140
+ }
141
+ }
142
+
143
+ private normalizeLimit(limit: number): number {
144
+ if (!Number.isFinite(limit)) return 0;
145
+ const clamped = Math.max(0, Math.floor(limit));
146
+ return Math.min(clamped, 1000);
147
+ }
148
+
149
+ private buildFtsQuery(query: string): string | null {
150
+ const tokens = query
151
+ .trim()
152
+ .split(/\s+/)
153
+ .map((token) => token.trim())
154
+ .filter(Boolean);
155
+
156
+ if (tokens.length === 0) return null;
157
+
158
+ return tokens
159
+ .map((token) => {
160
+ const escaped = token.replace(/"/g, '""');
161
+ return `"${escaped}"*`;
162
+ })
163
+ .join(" ");
164
+ }
165
+
166
+ private toEntry(row: HistoryRow): HistoryEntry {
167
+ return {
168
+ id: row.id,
169
+ prompt: row.prompt,
170
+ created_at: row.created_at,
171
+ cwd: row.cwd ?? undefined,
172
+ };
173
+ }
174
+ }
package/src/core/index.ts CHANGED
@@ -25,6 +25,7 @@ export {
25
25
  loadExtensionFromFactory,
26
26
  type ToolDefinition,
27
27
  } from "./extensions/index";
28
+ export { HistoryStorage } from "./history-storage";
28
29
  export {
29
30
  createMCPManager,
30
31
  discoverAndLoadMCPTools,
@@ -26,6 +26,7 @@ export type AppAction =
26
26
  | "expandTools"
27
27
  | "toggleThinking"
28
28
  | "externalEditor"
29
+ | "historySearch"
29
30
  | "followUp"
30
31
  | "dequeue";
31
32
 
@@ -53,6 +54,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
53
54
  cycleModelForward: "ctrl+p",
54
55
  cycleModelBackward: "shift+ctrl+p",
55
56
  selectModel: "ctrl+l",
57
+ historySearch: "ctrl+r",
56
58
  expandTools: "ctrl+o",
57
59
  toggleThinking: "ctrl+t",
58
60
  externalEditor: "ctrl+g",
@@ -78,6 +80,7 @@ const APP_ACTIONS: AppAction[] = [
78
80
  "cycleModelForward",
79
81
  "cycleModelBackward",
80
82
  "selectModel",
83
+ "historySearch",
81
84
  "expandTools",
82
85
  "toggleThinking",
83
86
  "externalEditor",