@oh-my-pi/pi-coding-agent 3.15.0 → 3.20.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 (193) hide show
  1. package/CHANGELOG.md +61 -1
  2. package/docs/extensions.md +1055 -0
  3. package/docs/rpc.md +69 -13
  4. package/docs/session-tree-plan.md +1 -1
  5. package/examples/extensions/README.md +141 -0
  6. package/examples/extensions/api-demo.ts +87 -0
  7. package/examples/extensions/chalk-logger.ts +26 -0
  8. package/examples/extensions/hello.ts +33 -0
  9. package/examples/extensions/pirate.ts +44 -0
  10. package/examples/extensions/plan-mode.ts +551 -0
  11. package/examples/extensions/subagent/agents/reviewer.md +35 -0
  12. package/examples/extensions/todo.ts +299 -0
  13. package/examples/extensions/tools.ts +145 -0
  14. package/examples/extensions/with-deps/index.ts +36 -0
  15. package/examples/extensions/with-deps/package-lock.json +31 -0
  16. package/examples/extensions/with-deps/package.json +16 -0
  17. package/examples/sdk/02-custom-model.ts +3 -3
  18. package/examples/sdk/05-tools.ts +7 -3
  19. package/examples/sdk/06-extensions.ts +81 -0
  20. package/examples/sdk/06-hooks.ts +14 -13
  21. package/examples/sdk/08-prompt-templates.ts +42 -0
  22. package/examples/sdk/08-slash-commands.ts +17 -12
  23. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  24. package/examples/sdk/12-full-control.ts +6 -6
  25. package/package.json +11 -7
  26. package/src/capability/extension-module.ts +34 -0
  27. package/src/cli/args.ts +22 -7
  28. package/src/cli/file-processor.ts +38 -67
  29. package/src/cli/list-models.ts +1 -1
  30. package/src/config.ts +25 -14
  31. package/src/core/agent-session.ts +505 -242
  32. package/src/core/auth-storage.ts +33 -21
  33. package/src/core/compaction/branch-summarization.ts +4 -4
  34. package/src/core/compaction/compaction.ts +3 -3
  35. package/src/core/custom-commands/bundled/wt/index.ts +430 -0
  36. package/src/core/custom-commands/loader.ts +9 -0
  37. package/src/core/custom-tools/wrapper.ts +5 -0
  38. package/src/core/event-bus.ts +59 -0
  39. package/src/core/export-html/vendor/highlight.min.js +1213 -0
  40. package/src/core/export-html/vendor/marked.min.js +6 -0
  41. package/src/core/extensions/index.ts +100 -0
  42. package/src/core/extensions/loader.ts +501 -0
  43. package/src/core/extensions/runner.ts +477 -0
  44. package/src/core/extensions/types.ts +712 -0
  45. package/src/core/extensions/wrapper.ts +147 -0
  46. package/src/core/hooks/types.ts +2 -2
  47. package/src/core/index.ts +10 -21
  48. package/src/core/keybindings.ts +199 -0
  49. package/src/core/messages.ts +26 -7
  50. package/src/core/model-registry.ts +123 -46
  51. package/src/core/model-resolver.ts +7 -5
  52. package/src/core/prompt-templates.ts +242 -0
  53. package/src/core/sdk.ts +378 -295
  54. package/src/core/session-manager.ts +72 -58
  55. package/src/core/settings-manager.ts +118 -22
  56. package/src/core/system-prompt.ts +24 -1
  57. package/src/core/terminal-notify.ts +37 -0
  58. package/src/core/tools/context.ts +4 -4
  59. package/src/core/tools/exa/mcp-client.ts +5 -4
  60. package/src/core/tools/exa/render.ts +176 -131
  61. package/src/core/tools/gemini-image.ts +361 -0
  62. package/src/core/tools/git.ts +216 -0
  63. package/src/core/tools/index.ts +28 -15
  64. package/src/core/tools/lsp/config.ts +5 -4
  65. package/src/core/tools/lsp/index.ts +17 -12
  66. package/src/core/tools/lsp/render.ts +39 -47
  67. package/src/core/tools/read.ts +66 -29
  68. package/src/core/tools/render-utils.ts +268 -0
  69. package/src/core/tools/renderers.ts +243 -225
  70. package/src/core/tools/task/discovery.ts +2 -2
  71. package/src/core/tools/task/executor.ts +66 -58
  72. package/src/core/tools/task/index.ts +29 -10
  73. package/src/core/tools/task/model-resolver.ts +8 -13
  74. package/src/core/tools/task/omp-command.ts +24 -0
  75. package/src/core/tools/task/render.ts +35 -60
  76. package/src/core/tools/task/types.ts +3 -0
  77. package/src/core/tools/web-fetch.ts +29 -28
  78. package/src/core/tools/web-search/index.ts +6 -5
  79. package/src/core/tools/web-search/providers/exa.ts +6 -5
  80. package/src/core/tools/web-search/render.ts +66 -111
  81. package/src/core/voice-controller.ts +135 -0
  82. package/src/core/voice-supervisor.ts +1003 -0
  83. package/src/core/voice.ts +308 -0
  84. package/src/discovery/builtin.ts +75 -1
  85. package/src/discovery/claude.ts +47 -1
  86. package/src/discovery/codex.ts +54 -2
  87. package/src/discovery/gemini.ts +55 -2
  88. package/src/discovery/helpers.ts +100 -1
  89. package/src/discovery/index.ts +2 -0
  90. package/src/index.ts +14 -9
  91. package/src/lib/worktree/collapse.ts +179 -0
  92. package/src/lib/worktree/constants.ts +14 -0
  93. package/src/lib/worktree/errors.ts +23 -0
  94. package/src/lib/worktree/git.ts +110 -0
  95. package/src/lib/worktree/index.ts +23 -0
  96. package/src/lib/worktree/operations.ts +216 -0
  97. package/src/lib/worktree/session.ts +114 -0
  98. package/src/lib/worktree/stats.ts +67 -0
  99. package/src/main.ts +61 -37
  100. package/src/migrations.ts +37 -7
  101. package/src/modes/interactive/components/bash-execution.ts +6 -4
  102. package/src/modes/interactive/components/custom-editor.ts +55 -0
  103. package/src/modes/interactive/components/custom-message.ts +95 -0
  104. package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
  105. package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
  106. package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
  107. package/src/modes/interactive/components/extensions/types.ts +1 -0
  108. package/src/modes/interactive/components/footer.ts +324 -0
  109. package/src/modes/interactive/components/hook-editor.ts +1 -0
  110. package/src/modes/interactive/components/hook-selector.ts +3 -3
  111. package/src/modes/interactive/components/model-selector.ts +7 -6
  112. package/src/modes/interactive/components/oauth-selector.ts +3 -3
  113. package/src/modes/interactive/components/settings-defs.ts +55 -6
  114. package/src/modes/interactive/components/status-line/separators.ts +4 -4
  115. package/src/modes/interactive/components/status-line.ts +45 -35
  116. package/src/modes/interactive/components/tool-execution.ts +95 -23
  117. package/src/modes/interactive/interactive-mode.ts +644 -113
  118. package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
  119. package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
  120. package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
  121. package/src/modes/interactive/theme/defaults/basalt.json +90 -0
  122. package/src/modes/interactive/theme/defaults/birch.json +101 -0
  123. package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
  124. package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
  125. package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
  126. package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
  127. package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
  128. package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
  129. package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
  130. package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
  131. package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
  132. package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
  133. package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
  134. package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
  135. package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
  136. package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
  137. package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
  138. package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
  139. package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
  140. package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
  141. package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
  142. package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
  143. package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
  144. package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
  145. package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
  146. package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
  147. package/src/modes/interactive/theme/defaults/graphite.json +99 -0
  148. package/src/modes/interactive/theme/defaults/index.ts +128 -0
  149. package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
  150. package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
  151. package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
  152. package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
  153. package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
  154. package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
  155. package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
  156. package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
  157. package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
  158. package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
  159. package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
  160. package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
  161. package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
  162. package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
  163. package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
  164. package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
  165. package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
  166. package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
  167. package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
  168. package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
  169. package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
  170. package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
  171. package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
  172. package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
  173. package/src/modes/interactive/theme/defaults/limestone.json +100 -0
  174. package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
  175. package/src/modes/interactive/theme/defaults/marble.json +99 -0
  176. package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
  177. package/src/modes/interactive/theme/defaults/onyx.json +90 -0
  178. package/src/modes/interactive/theme/defaults/pearl.json +99 -0
  179. package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
  180. package/src/modes/interactive/theme/defaults/quartz.json +102 -0
  181. package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
  182. package/src/modes/interactive/theme/defaults/titanium.json +89 -0
  183. package/src/modes/print-mode.ts +14 -72
  184. package/src/modes/rpc/rpc-client.ts +23 -9
  185. package/src/modes/rpc/rpc-mode.ts +137 -125
  186. package/src/modes/rpc/rpc-types.ts +46 -24
  187. package/src/prompts/task.md +1 -0
  188. package/src/prompts/tools/gemini-image.md +4 -0
  189. package/src/prompts/tools/git.md +9 -0
  190. package/src/prompts/voice-summary.md +12 -0
  191. package/src/utils/image-convert.ts +26 -0
  192. package/src/utils/image-resize.ts +215 -0
  193. package/src/utils/shell-snapshot.ts +22 -20
@@ -0,0 +1,216 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { WORKTREE_BASE } from "./constants";
4
+ import { WorktreeError, WorktreeErrorCode } from "./errors";
5
+ import { getRepoName, getRepoRoot, git } from "./git";
6
+
7
+ export interface Worktree {
8
+ path: string;
9
+ branch: string | null;
10
+ head: string;
11
+ isMain: boolean;
12
+ isDetached: boolean;
13
+ }
14
+
15
+ type WorktreePartial = Partial<Worktree> & { isDetached?: boolean };
16
+
17
+ function finalizeWorktree(entry: WorktreePartial, repoRoot: string): Worktree {
18
+ const wtPath = entry.path?.trim();
19
+ if (!wtPath) {
20
+ throw new Error("Invalid worktree entry");
21
+ }
22
+ const branch = entry.isDetached ? null : (entry.branch ?? null);
23
+ const isDetached = entry.isDetached ?? branch === null;
24
+ return {
25
+ path: wtPath,
26
+ branch,
27
+ head: entry.head ?? "",
28
+ isMain: path.resolve(wtPath) === path.resolve(repoRoot),
29
+ isDetached,
30
+ };
31
+ }
32
+
33
+ function parseWorktreeList(output: string, repoRoot: string): Worktree[] {
34
+ const worktrees: Worktree[] = [];
35
+ let current: WorktreePartial = {};
36
+
37
+ for (const line of output.split("\n")) {
38
+ if (line.startsWith("worktree ")) {
39
+ if (current.path) {
40
+ worktrees.push(finalizeWorktree(current, repoRoot));
41
+ }
42
+ current = { path: line.slice(9) };
43
+ continue;
44
+ }
45
+
46
+ if (line.startsWith("HEAD ")) {
47
+ current.head = line.slice(5);
48
+ continue;
49
+ }
50
+
51
+ if (line.startsWith("branch ")) {
52
+ const raw = line.slice(7);
53
+ current.branch = raw.startsWith("refs/heads/") ? raw.slice("refs/heads/".length) : raw;
54
+ continue;
55
+ }
56
+
57
+ if (line === "detached") {
58
+ current.isDetached = true;
59
+ }
60
+ }
61
+
62
+ if (current.path) {
63
+ worktrees.push(finalizeWorktree(current, repoRoot));
64
+ }
65
+
66
+ return worktrees;
67
+ }
68
+
69
+ /**
70
+ * Create a new worktree.
71
+ */
72
+ export async function create(branch: string, options?: { base?: string; path?: string }): Promise<Worktree> {
73
+ const repoRoot = await getRepoRoot();
74
+ const repoName = await getRepoName();
75
+ const targetPath = options?.path ?? path.join(WORKTREE_BASE, repoName, branch);
76
+ const resolvedTarget = path.resolve(targetPath);
77
+
78
+ const existing = await list();
79
+ const conflict = existing.find((wt) => wt.branch === branch || path.resolve(wt.path) === resolvedTarget);
80
+ if (conflict) {
81
+ throw new WorktreeError(`Worktree already exists: ${conflict.path}`, WorktreeErrorCode.WORKTREE_EXISTS);
82
+ }
83
+
84
+ await mkdir(path.dirname(resolvedTarget), { recursive: true });
85
+
86
+ const branchExists = (await git(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot)).code === 0;
87
+
88
+ const args = branchExists
89
+ ? ["worktree", "add", resolvedTarget, branch]
90
+ : ["worktree", "add", "-b", branch, resolvedTarget, options?.base ?? "HEAD"];
91
+
92
+ const result = await git(args, repoRoot);
93
+ if (result.code !== 0) {
94
+ const stderr = result.stderr.trim();
95
+ if (stderr.includes("already exists") || stderr.includes("already checked out")) {
96
+ throw new WorktreeError(stderr || "Worktree already exists", WorktreeErrorCode.WORKTREE_EXISTS);
97
+ }
98
+ throw new Error(stderr || "Failed to create worktree");
99
+ }
100
+
101
+ const updated = await list();
102
+ const created = updated.find((wt) => path.resolve(wt.path) === resolvedTarget);
103
+ if (!created) {
104
+ throw new Error("Worktree created but not found in list");
105
+ }
106
+
107
+ return created;
108
+ }
109
+
110
+ /**
111
+ * List all worktrees for current repository.
112
+ */
113
+ export async function list(): Promise<Worktree[]> {
114
+ const repoRoot = await getRepoRoot();
115
+ const result = await git(["worktree", "list", "--porcelain"], repoRoot);
116
+ if (result.code !== 0) {
117
+ throw new Error(result.stderr.trim() || "Failed to list worktrees");
118
+ }
119
+ return parseWorktreeList(result.stdout, repoRoot);
120
+ }
121
+
122
+ /**
123
+ * Find a worktree by pattern.
124
+ */
125
+ export async function find(pattern: string): Promise<Worktree> {
126
+ const worktrees = await list();
127
+
128
+ const exactBranch = worktrees.filter((wt) => wt.branch === pattern);
129
+ if (exactBranch.length === 1) return exactBranch[0];
130
+ if (exactBranch.length > 1) {
131
+ throw new WorktreeError(`Ambiguous worktree: ${pattern}`, WorktreeErrorCode.WORKTREE_NOT_FOUND);
132
+ }
133
+
134
+ const exactDir = worktrees.filter((wt) => path.basename(wt.path) === pattern);
135
+ if (exactDir.length === 1) return exactDir[0];
136
+ if (exactDir.length > 1) {
137
+ throw new WorktreeError(`Ambiguous worktree: ${pattern}`, WorktreeErrorCode.WORKTREE_NOT_FOUND);
138
+ }
139
+
140
+ const partialBranch = worktrees.filter((wt) => wt.branch?.includes(pattern));
141
+ if (partialBranch.length === 1) return partialBranch[0];
142
+ if (partialBranch.length > 1) {
143
+ throw new WorktreeError(`Ambiguous worktree: ${pattern}`, WorktreeErrorCode.WORKTREE_NOT_FOUND);
144
+ }
145
+
146
+ const partialPath = worktrees.filter((wt) => wt.path.includes(pattern));
147
+ if (partialPath.length === 1) return partialPath[0];
148
+ if (partialPath.length > 1) {
149
+ throw new WorktreeError(`Ambiguous worktree: ${pattern}`, WorktreeErrorCode.WORKTREE_NOT_FOUND);
150
+ }
151
+
152
+ throw new WorktreeError(`Worktree not found: ${pattern}`, WorktreeErrorCode.WORKTREE_NOT_FOUND);
153
+ }
154
+
155
+ /**
156
+ * Remove a worktree.
157
+ */
158
+ export async function remove(nameOrPath: string, options?: { force?: boolean }): Promise<void> {
159
+ const wt = await find(nameOrPath);
160
+ if (wt.isMain) {
161
+ throw new WorktreeError("Cannot remove main worktree", WorktreeErrorCode.CANNOT_MODIFY_MAIN);
162
+ }
163
+
164
+ const repoRoot = await getRepoRoot();
165
+ const args = ["worktree", "remove", wt.path];
166
+ if (options?.force) args.push("--force");
167
+
168
+ const result = await git(args, repoRoot);
169
+ if (result.code !== 0) {
170
+ throw new Error(result.stderr.trim() || "Failed to remove worktree");
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Remove worktrees for branches that no longer exist.
176
+ */
177
+ export async function prune(): Promise<number> {
178
+ const repoRoot = await getRepoRoot();
179
+ const worktrees = await list();
180
+ let removed = 0;
181
+
182
+ for (const wt of worktrees) {
183
+ if (wt.isMain || !wt.branch) continue;
184
+ const existsResult = await git(["rev-parse", "--verify", `refs/heads/${wt.branch}`], repoRoot);
185
+ if (existsResult.code === 0) continue;
186
+
187
+ const result = await git(["worktree", "remove", wt.path], repoRoot);
188
+ if (result.code !== 0) {
189
+ throw new Error(result.stderr.trim() || `Failed to remove worktree: ${wt.path}`);
190
+ }
191
+ removed += 1;
192
+ }
193
+
194
+ return removed;
195
+ }
196
+
197
+ /**
198
+ * Get the worktree containing the given path.
199
+ * Returns null if path is not in any worktree.
200
+ */
201
+ export async function which(targetPath?: string): Promise<Worktree | null> {
202
+ const worktrees = await list();
203
+ const resolved = path.resolve(targetPath ?? process.cwd());
204
+
205
+ let best: Worktree | null = null;
206
+ for (const wt of worktrees) {
207
+ const wtPath = path.resolve(wt.path);
208
+ if (resolved === wtPath || resolved.startsWith(wtPath + path.sep)) {
209
+ if (!best || wtPath.length > best.path.length) {
210
+ best = wt;
211
+ }
212
+ }
213
+ }
214
+
215
+ return best;
216
+ }
@@ -0,0 +1,114 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { nanoid } from "nanoid";
4
+ import { getRepoRoot, git } from "./git";
5
+
6
+ export interface WorktreeSession {
7
+ id: string;
8
+ branch: string;
9
+ path: string;
10
+ scope?: string[];
11
+ agentId?: string;
12
+ task?: string;
13
+ status: SessionStatus;
14
+ createdAt: number;
15
+ completedAt?: number;
16
+ }
17
+
18
+ export type SessionStatus = "creating" | "active" | "completed" | "merging" | "merged" | "failed" | "abandoned";
19
+
20
+ async function getSessionsFile(): Promise<string> {
21
+ const repoRoot = await getRepoRoot();
22
+ const result = await git(["rev-parse", "--git-common-dir"], repoRoot);
23
+ let gitDir = result.code === 0 ? result.stdout.trim() : "";
24
+ if (!gitDir) {
25
+ gitDir = path.join(repoRoot, ".git");
26
+ }
27
+ if (!path.isAbsolute(gitDir)) {
28
+ // Resolve relative git dir from repo root to keep sessions in the common dir.
29
+ gitDir = path.resolve(repoRoot, gitDir);
30
+ }
31
+ await mkdir(gitDir, { recursive: true });
32
+ return path.join(gitDir, "worktree-sessions.json");
33
+ }
34
+
35
+ async function loadSessions(): Promise<WorktreeSession[]> {
36
+ const filePath = await getSessionsFile();
37
+ const file = Bun.file(filePath);
38
+ if (!(await file.exists())) return [];
39
+ try {
40
+ const data = await file.json();
41
+ if (Array.isArray(data)) {
42
+ return data as WorktreeSession[];
43
+ }
44
+ } catch {
45
+ return [];
46
+ }
47
+ return [];
48
+ }
49
+
50
+ async function saveSessions(sessions: WorktreeSession[]): Promise<void> {
51
+ const filePath = await getSessionsFile();
52
+ await Bun.write(filePath, JSON.stringify(sessions, null, 2));
53
+ }
54
+
55
+ export async function createSession(params: {
56
+ branch: string;
57
+ path: string;
58
+ scope?: string[];
59
+ task?: string;
60
+ }): Promise<WorktreeSession> {
61
+ const sessions = await loadSessions();
62
+ const session: WorktreeSession = {
63
+ id: nanoid(10),
64
+ branch: params.branch,
65
+ path: params.path,
66
+ scope: params.scope,
67
+ task: params.task,
68
+ status: "creating",
69
+ createdAt: Date.now(),
70
+ };
71
+
72
+ sessions.push(session);
73
+ await saveSessions(sessions);
74
+ return session;
75
+ }
76
+
77
+ export async function updateSession(id: string, updates: Partial<WorktreeSession>): Promise<void> {
78
+ const sessions = await loadSessions();
79
+ const idx = sessions.findIndex((s) => s.id === id);
80
+ if (idx === -1) return;
81
+ const current = sessions[idx];
82
+ sessions[idx] = { ...current, ...updates, id: current.id };
83
+ await saveSessions(sessions);
84
+ }
85
+
86
+ export async function getSession(id: string): Promise<WorktreeSession | null> {
87
+ const sessions = await loadSessions();
88
+ return sessions.find((s) => s.id === id) ?? null;
89
+ }
90
+
91
+ export async function listSessions(): Promise<WorktreeSession[]> {
92
+ return loadSessions();
93
+ }
94
+
95
+ export async function cleanupSessions(): Promise<number> {
96
+ const sessions = await loadSessions();
97
+ let removed = 0;
98
+
99
+ const remaining: WorktreeSession[] = [];
100
+ for (const session of sessions) {
101
+ const exists = await Bun.file(session.path).exists();
102
+ if (!exists) {
103
+ removed += 1;
104
+ continue;
105
+ }
106
+ remaining.push(session);
107
+ }
108
+
109
+ if (removed > 0) {
110
+ await saveSessions(remaining);
111
+ }
112
+
113
+ return removed;
114
+ }
@@ -0,0 +1,67 @@
1
+ import { git } from "./git";
2
+
3
+ export interface WorktreeStats {
4
+ additions: number;
5
+ deletions: number;
6
+ untracked: number;
7
+ modified: number;
8
+ staged: number;
9
+ }
10
+
11
+ /**
12
+ * Get diff statistics for a worktree.
13
+ */
14
+ export async function getStats(worktreePath: string): Promise<WorktreeStats> {
15
+ const diffResult = await git(["diff", "HEAD", "--shortstat"], worktreePath);
16
+
17
+ let additions = 0;
18
+ let deletions = 0;
19
+
20
+ const statsLine = diffResult.stdout.trim();
21
+ if (statsLine) {
22
+ const insertMatch = statsLine.match(/(\d+) insertion/);
23
+ const deleteMatch = statsLine.match(/(\d+) deletion/);
24
+ if (insertMatch) additions = parseInt(insertMatch[1], 10);
25
+ if (deleteMatch) deletions = parseInt(deleteMatch[1], 10);
26
+ }
27
+
28
+ const untrackedResult = await git(["ls-files", "--others", "--exclude-standard"], worktreePath);
29
+ const untracked = untrackedResult.stdout.trim() ? untrackedResult.stdout.trim().split("\n").length : 0;
30
+
31
+ const statusResult = await git(["status", "--porcelain"], worktreePath);
32
+ let modified = 0;
33
+ let staged = 0;
34
+
35
+ for (const line of statusResult.stdout.split("\n")) {
36
+ if (!line) continue;
37
+ const index = line[0];
38
+ const worktree = line[1];
39
+ if (index !== " " && index !== "?") staged += 1;
40
+ if (worktree !== " " && worktree !== "?") modified += 1;
41
+ }
42
+
43
+ return { additions, deletions, untracked, modified, staged };
44
+ }
45
+
46
+ /**
47
+ * Format stats for display.
48
+ * Returns "clean" or "+N -M ?U" format.
49
+ */
50
+ export function formatStats(stats: WorktreeStats): string {
51
+ if (
52
+ stats.additions === 0 &&
53
+ stats.deletions === 0 &&
54
+ stats.untracked === 0 &&
55
+ stats.modified === 0 &&
56
+ stats.staged === 0
57
+ ) {
58
+ return "clean";
59
+ }
60
+
61
+ const parts: string[] = [];
62
+ if (stats.additions > 0) parts.push(`+${stats.additions}`);
63
+ if (stats.deletions > 0) parts.push(`-${stats.deletions}`);
64
+ if (stats.untracked > 0) parts.push(`?${stats.untracked}`);
65
+
66
+ return parts.join(" ") || "clean";
67
+ }
package/src/main.ts CHANGED
@@ -15,9 +15,8 @@ import { selectSession } from "./cli/session-picker";
15
15
  import { parseUpdateArgs, printUpdateHelp, runUpdateCommand } from "./cli/update-cli";
16
16
  import { findConfigFile, getModelsPath, VERSION } from "./config";
17
17
  import type { AgentSession } from "./core/agent-session";
18
- import type { LoadedCustomTool } from "./core/custom-tools/index";
19
18
  import { exportFromFile } from "./core/export-html/index";
20
- import type { HookUIContext } from "./core/index";
19
+ import type { ExtensionUIContext } from "./core/index";
21
20
  import type { ModelRegistry } from "./core/model-registry";
22
21
  import { parseModelPattern, resolveModelScope, type ScopedModel } from "./core/model-resolver";
23
22
  import { type CreateAgentSessionOptions, createAgentSession, discoverAuthStorage, discoverModels } from "./core/sdk";
@@ -26,7 +25,7 @@ import { SettingsManager } from "./core/settings-manager";
26
25
  import { resolvePromptInput } from "./core/system-prompt";
27
26
  import { printTimings, time } from "./core/timings";
28
27
  import { allTools } from "./core/tools/index";
29
- import { runMigrations } from "./migrations";
28
+ import { runMigrations, showDeprecationWarnings } from "./migrations";
30
29
  import { InteractiveMode, installTerminalCrashHandlers, runPrintMode, runRpcMode } from "./modes/index";
31
30
  import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme";
32
31
  import { getChangelogPath, getNewEntries, parseChangelog } from "./utils/changelog";
@@ -59,22 +58,13 @@ async function runInteractiveMode(
59
58
  migratedProviders: string[],
60
59
  versionCheckPromise: Promise<string | undefined>,
61
60
  initialMessages: string[],
62
- customTools: LoadedCustomTool[],
63
- setToolUIContext: (uiContext: HookUIContext, hasUI: boolean) => void,
61
+ setExtensionUIContext: (uiContext: ExtensionUIContext, hasUI: boolean) => void,
64
62
  lspServers: Array<{ name: string; status: "ready" | "error"; fileTypes: string[] }> | undefined,
65
63
  initialMessage?: string,
66
64
  initialImages?: ImageContent[],
67
65
  fdPath: string | undefined = undefined,
68
66
  ): Promise<void> {
69
- const mode = new InteractiveMode(
70
- session,
71
- version,
72
- changelogMarkdown,
73
- customTools,
74
- setToolUIContext,
75
- lspServers,
76
- fdPath,
77
- );
67
+ const mode = new InteractiveMode(session, version, changelogMarkdown, setExtensionUIContext, lspServers, fdPath);
78
68
 
79
69
  await mode.init();
80
70
 
@@ -127,7 +117,10 @@ async function runInteractiveMode(
127
117
  }
128
118
  }
129
119
 
130
- async function prepareInitialMessage(parsed: Args): Promise<{
120
+ async function prepareInitialMessage(
121
+ parsed: Args,
122
+ autoResizeImages: boolean,
123
+ ): Promise<{
131
124
  initialMessage?: string;
132
125
  initialImages?: ImageContent[];
133
126
  }> {
@@ -135,7 +128,7 @@ async function prepareInitialMessage(parsed: Args): Promise<{
135
128
  return {};
136
129
  }
137
130
 
138
- const { text, images } = await processFileArguments(parsed.fileArgs);
131
+ const { text, images } = await processFileArguments(parsed.fileArgs, { autoResizeImages });
139
132
 
140
133
  let initialMessage: string;
141
134
  if (parsed.messages.length > 0) {
@@ -215,6 +208,7 @@ async function buildSessionOptions(
215
208
  scopedModels: ScopedModel[],
216
209
  sessionManager: SessionManager | undefined,
217
210
  modelRegistry: ModelRegistry,
211
+ settingsManager: SettingsManager,
218
212
  ): Promise<CreateAgentSessionOptions> {
219
213
  const options: CreateAgentSessionOptions = {};
220
214
 
@@ -229,7 +223,7 @@ async function buildSessionOptions(
229
223
 
230
224
  // Model from CLI (--model) - uses same fuzzy matching as --models
231
225
  if (parsed.model) {
232
- const available = await modelRegistry.getAvailable();
226
+ const available = modelRegistry.getAvailable();
233
227
  const { model, warning } = parseModelPattern(parsed.model, available);
234
228
  if (warning) {
235
229
  console.warn(chalk.yellow(`Warning: ${warning}`));
@@ -276,16 +270,20 @@ async function buildSessionOptions(
276
270
  // Skills
277
271
  if (parsed.noSkills) {
278
272
  options.skills = [];
273
+ } else if (parsed.skills && parsed.skills.length > 0) {
274
+ // Override includeSkills in settingsManager for this session
275
+ settingsManager.applyOverrides({
276
+ skills: {
277
+ ...settingsManager.getSkillsSettings(),
278
+ includeSkills: parsed.skills,
279
+ },
280
+ });
279
281
  }
280
282
 
281
- // Additional hook paths from CLI
282
- if (parsed.hooks && parsed.hooks.length > 0) {
283
- options.additionalHookPaths = parsed.hooks;
284
- }
285
-
286
- // Additional custom tool paths from CLI
287
- if (parsed.customTools && parsed.customTools.length > 0) {
288
- options.additionalCustomToolPaths = parsed.customTools;
283
+ // Additional extension paths from CLI
284
+ const cliExtensionPaths = [...(parsed.extensions ?? []), ...(parsed.hooks ?? [])];
285
+ if (cliExtensionPaths.length > 0) {
286
+ options.additionalExtensionPaths = cliExtensionPaths;
289
287
  }
290
288
 
291
289
  return options;
@@ -320,12 +318,12 @@ export async function main(args: string[]) {
320
318
  return;
321
319
  }
322
320
 
323
- // Run migrations
324
- const { migratedAuthProviders: migratedProviders } = runMigrations();
321
+ // Run migrations (pass cwd for project-local migrations)
322
+ const { migratedAuthProviders: migratedProviders, deprecationWarnings } = await runMigrations(process.cwd());
325
323
 
326
324
  // Create AuthStorage and ModelRegistry upfront
327
- const authStorage = discoverAuthStorage();
328
- const modelRegistry = discoverModels(authStorage);
325
+ const authStorage = await discoverAuthStorage();
326
+ const modelRegistry = await discoverModels(authStorage);
329
327
  time("discoverModels");
330
328
 
331
329
  const parsed = parseArgs(args);
@@ -366,14 +364,13 @@ export async function main(args: string[]) {
366
364
  }
367
365
 
368
366
  const cwd = process.cwd();
369
- const { initialMessage, initialImages } = await prepareInitialMessage(parsed);
367
+ const settingsManager = SettingsManager.create(cwd);
368
+ time("SettingsManager.create");
369
+ const { initialMessage, initialImages } = await prepareInitialMessage(parsed, settingsManager.getImageAutoResize());
370
370
  time("prepareInitialMessage");
371
371
  const isInteractive = !parsed.print && parsed.mode === undefined;
372
372
  const mode = parsed.mode || "text";
373
373
 
374
- const settingsManager = SettingsManager.create(cwd);
375
- time("SettingsManager.create");
376
-
377
374
  // Initialize discovery system with settings for provider persistence
378
375
  const { initializeWithSettings } = await import("./discovery");
379
376
  initializeWithSettings(settingsManager);
@@ -392,6 +389,11 @@ export async function main(args: string[]) {
392
389
  initTheme(settingsManager.getTheme(), isInteractive, settingsManager.getSymbolPreset());
393
390
  time("initTheme");
394
391
 
392
+ // Show deprecation warnings in interactive mode
393
+ if (isInteractive && deprecationWarnings.length > 0) {
394
+ await showDeprecationWarnings(deprecationWarnings);
395
+ }
396
+
395
397
  let scopedModels: ScopedModel[] = [];
396
398
  const modelPatterns = parsed.models ?? settingsManager.getEnabledModels();
397
399
  if (modelPatterns && modelPatterns.length > 0) {
@@ -420,9 +422,16 @@ export async function main(args: string[]) {
420
422
  sessionManager = await SessionManager.open(selectedPath);
421
423
  }
422
424
 
423
- const sessionOptions = await buildSessionOptions(parsed, scopedModels, sessionManager, modelRegistry);
425
+ const sessionOptions = await buildSessionOptions(
426
+ parsed,
427
+ scopedModels,
428
+ sessionManager,
429
+ modelRegistry,
430
+ settingsManager,
431
+ );
424
432
  sessionOptions.authStorage = authStorage;
425
433
  sessionOptions.modelRegistry = modelRegistry;
434
+ sessionOptions.settingsManager = settingsManager;
426
435
  sessionOptions.hasUI = isInteractive;
427
436
 
428
437
  // Handle CLI --api-key as runtime override (not persisted)
@@ -435,9 +444,25 @@ export async function main(args: string[]) {
435
444
  }
436
445
 
437
446
  time("buildSessionOptions");
438
- const { session, customToolsResult, modelFallbackMessage, lspServers } = await createAgentSession(sessionOptions);
447
+ const { session, extensionsResult, modelFallbackMessage, lspServers } = await createAgentSession(sessionOptions);
439
448
  time("createAgentSession");
440
449
 
450
+ // Re-parse CLI args with extension flags and apply values
451
+ if (session.extensionRunner) {
452
+ const extFlags = session.extensionRunner.getFlags();
453
+ if (extFlags.size > 0) {
454
+ const flagDefs = new Map<string, { type: "boolean" | "string" }>();
455
+ for (const [name, flag] of extFlags) {
456
+ flagDefs.set(name, { type: flag.type });
457
+ }
458
+ const reparsed = parseArgs(args, flagDefs);
459
+ for (const [name, value] of reparsed.unknownFlags) {
460
+ session.extensionRunner.setFlagValue(name, value);
461
+ }
462
+ }
463
+ }
464
+ time("applyExtensionFlags");
465
+
441
466
  if (!isInteractive && !session.model) {
442
467
  console.error(chalk.red("No models available."));
443
468
  console.error(chalk.yellow("\nSet an API key environment variable:"));
@@ -489,8 +514,7 @@ export async function main(args: string[]) {
489
514
  migratedProviders,
490
515
  versionCheckPromise,
491
516
  parsed.messages,
492
- customToolsResult.tools,
493
- customToolsResult.setUIContext,
517
+ extensionsResult.setUIContext,
494
518
  lspServers,
495
519
  initialMessage,
496
520
  initialImages,
package/src/migrations.ts CHANGED
@@ -4,22 +4,28 @@
4
4
 
5
5
  import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
+ import chalk from "chalk";
7
8
  import { getAgentDir } from "./config";
8
9
 
9
10
  /**
10
11
  * Migrate PI_* environment variables to OMP_* equivalents.
11
12
  * If PI_XX is set and OMP_XX is not, set OMP_XX to PI_XX's value.
12
13
  * This provides backwards compatibility for users with existing PI_* env vars.
14
+ *
15
+ * @returns Array of PI_* env var names that were migrated
13
16
  */
14
- export function migrateEnvVars(): void {
17
+ export function migrateEnvVars(): string[] {
18
+ const migrated: string[] = [];
15
19
  for (const [key, value] of Object.entries(process.env)) {
16
20
  if (key.startsWith("PI_") && value !== undefined) {
17
21
  const ompKey = `OMP_${key.slice(3)}`; // PI_FOO -> OMP_FOO
18
22
  if (process.env[ompKey] === undefined) {
19
23
  process.env[ompKey] = value;
24
+ migrated.push(key);
20
25
  }
21
26
  }
22
27
  }
28
+ return migrated;
23
29
  }
24
30
 
25
31
  /**
@@ -122,9 +128,7 @@ export function migrateSessionsFromAgentRoot(): void {
122
128
  const correctDir = join(agentDir, "sessions", safePath);
123
129
 
124
130
  // Create directory if needed
125
- if (!existsSync(correctDir)) {
126
- mkdirSync(correctDir, { recursive: true });
127
- }
131
+ mkdirSync(correctDir, { recursive: true });
128
132
 
129
133
  // Move the file
130
134
  const fileName = file.split("/").pop() || file.split("\\").pop();
@@ -142,15 +146,41 @@ export function migrateSessionsFromAgentRoot(): void {
142
146
  /**
143
147
  * Run all migrations. Called once on startup.
144
148
  *
149
+ * @param _cwd - Current working directory (reserved for future project-local migrations)
145
150
  * @returns Object with migration results
146
151
  */
147
- export function runMigrations(): { migratedAuthProviders: string[] } {
152
+ export async function runMigrations(_cwd: string): Promise<{
153
+ migratedAuthProviders: string[];
154
+ deprecationWarnings: string[];
155
+ }> {
148
156
  // First: migrate env vars (before anything else reads them)
149
- migrateEnvVars();
157
+ const migratedEnvVars = migrateEnvVars();
150
158
 
151
159
  // Then: run data migrations
152
160
  const migratedAuthProviders = migrateAuthToAuthJson();
153
161
  migrateSessionsFromAgentRoot();
154
162
 
155
- return { migratedAuthProviders };
163
+ // Collect deprecation warnings
164
+ const deprecationWarnings: string[] = [];
165
+ if (migratedEnvVars.length > 0) {
166
+ for (const envVar of migratedEnvVars) {
167
+ const ompVar = `OMP_${envVar.slice(3)}`;
168
+ deprecationWarnings.push(`${envVar} is deprecated. Use ${ompVar} instead.`);
169
+ }
170
+ }
171
+
172
+ return { migratedAuthProviders, deprecationWarnings };
173
+ }
174
+
175
+ /**
176
+ * Display deprecation warnings to the user in interactive mode.
177
+ *
178
+ * @param warnings - Array of deprecation warning messages
179
+ */
180
+ export async function showDeprecationWarnings(warnings: string[]): Promise<void> {
181
+ console.log(chalk.yellow("\n⚠ Deprecation Warnings:"));
182
+ for (const warning of warnings) {
183
+ console.log(chalk.yellow(` • ${warning}`));
184
+ }
185
+ console.log();
156
186
  }