@oh-my-pi/pi-coding-agent 3.15.1 → 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.
- package/CHANGELOG.md +51 -1
- package/docs/extensions.md +1055 -0
- package/docs/rpc.md +69 -13
- package/docs/session-tree-plan.md +1 -1
- package/examples/extensions/README.md +141 -0
- package/examples/extensions/api-demo.ts +87 -0
- package/examples/extensions/chalk-logger.ts +26 -0
- package/examples/extensions/hello.ts +33 -0
- package/examples/extensions/pirate.ts +44 -0
- package/examples/extensions/plan-mode.ts +551 -0
- package/examples/extensions/subagent/agents/reviewer.md +35 -0
- package/examples/extensions/todo.ts +299 -0
- package/examples/extensions/tools.ts +145 -0
- package/examples/extensions/with-deps/index.ts +36 -0
- package/examples/extensions/with-deps/package-lock.json +31 -0
- package/examples/extensions/with-deps/package.json +16 -0
- package/examples/sdk/02-custom-model.ts +3 -3
- package/examples/sdk/05-tools.ts +7 -3
- package/examples/sdk/06-extensions.ts +81 -0
- package/examples/sdk/06-hooks.ts +14 -13
- package/examples/sdk/08-prompt-templates.ts +42 -0
- package/examples/sdk/08-slash-commands.ts +17 -12
- package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
- package/examples/sdk/12-full-control.ts +6 -6
- package/package.json +11 -7
- package/src/capability/extension-module.ts +34 -0
- package/src/cli/args.ts +22 -7
- package/src/cli/file-processor.ts +38 -67
- package/src/cli/list-models.ts +1 -1
- package/src/config.ts +25 -14
- package/src/core/agent-session.ts +505 -242
- package/src/core/auth-storage.ts +33 -21
- package/src/core/compaction/branch-summarization.ts +4 -4
- package/src/core/compaction/compaction.ts +3 -3
- package/src/core/custom-commands/bundled/wt/index.ts +430 -0
- package/src/core/custom-commands/loader.ts +9 -0
- package/src/core/custom-tools/wrapper.ts +5 -0
- package/src/core/event-bus.ts +59 -0
- package/src/core/export-html/vendor/highlight.min.js +1213 -0
- package/src/core/export-html/vendor/marked.min.js +6 -0
- package/src/core/extensions/index.ts +100 -0
- package/src/core/extensions/loader.ts +501 -0
- package/src/core/extensions/runner.ts +477 -0
- package/src/core/extensions/types.ts +712 -0
- package/src/core/extensions/wrapper.ts +147 -0
- package/src/core/hooks/types.ts +2 -2
- package/src/core/index.ts +10 -21
- package/src/core/keybindings.ts +199 -0
- package/src/core/messages.ts +26 -7
- package/src/core/model-registry.ts +123 -46
- package/src/core/model-resolver.ts +7 -5
- package/src/core/prompt-templates.ts +242 -0
- package/src/core/sdk.ts +378 -295
- package/src/core/session-manager.ts +72 -58
- package/src/core/settings-manager.ts +118 -22
- package/src/core/system-prompt.ts +24 -1
- package/src/core/terminal-notify.ts +37 -0
- package/src/core/tools/context.ts +4 -4
- package/src/core/tools/exa/mcp-client.ts +5 -4
- package/src/core/tools/exa/render.ts +176 -131
- package/src/core/tools/gemini-image.ts +361 -0
- package/src/core/tools/git.ts +216 -0
- package/src/core/tools/index.ts +28 -15
- package/src/core/tools/lsp/config.ts +5 -4
- package/src/core/tools/lsp/index.ts +17 -12
- package/src/core/tools/lsp/render.ts +39 -47
- package/src/core/tools/read.ts +66 -29
- package/src/core/tools/render-utils.ts +268 -0
- package/src/core/tools/renderers.ts +243 -225
- package/src/core/tools/task/discovery.ts +2 -2
- package/src/core/tools/task/executor.ts +66 -58
- package/src/core/tools/task/index.ts +29 -10
- package/src/core/tools/task/model-resolver.ts +8 -13
- package/src/core/tools/task/omp-command.ts +24 -0
- package/src/core/tools/task/render.ts +35 -60
- package/src/core/tools/task/types.ts +3 -0
- package/src/core/tools/web-fetch.ts +29 -28
- package/src/core/tools/web-search/index.ts +6 -5
- package/src/core/tools/web-search/providers/exa.ts +6 -5
- package/src/core/tools/web-search/render.ts +66 -111
- package/src/core/voice-controller.ts +135 -0
- package/src/core/voice-supervisor.ts +1003 -0
- package/src/core/voice.ts +308 -0
- package/src/discovery/builtin.ts +75 -1
- package/src/discovery/claude.ts +47 -1
- package/src/discovery/codex.ts +54 -2
- package/src/discovery/gemini.ts +55 -2
- package/src/discovery/helpers.ts +100 -1
- package/src/discovery/index.ts +2 -0
- package/src/index.ts +14 -9
- package/src/lib/worktree/collapse.ts +179 -0
- package/src/lib/worktree/constants.ts +14 -0
- package/src/lib/worktree/errors.ts +23 -0
- package/src/lib/worktree/git.ts +110 -0
- package/src/lib/worktree/index.ts +23 -0
- package/src/lib/worktree/operations.ts +216 -0
- package/src/lib/worktree/session.ts +114 -0
- package/src/lib/worktree/stats.ts +67 -0
- package/src/main.ts +61 -37
- package/src/migrations.ts +37 -7
- package/src/modes/interactive/components/bash-execution.ts +6 -4
- package/src/modes/interactive/components/custom-editor.ts +55 -0
- package/src/modes/interactive/components/custom-message.ts +95 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +5 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +18 -12
- package/src/modes/interactive/components/extensions/state-manager.ts +12 -0
- package/src/modes/interactive/components/extensions/types.ts +1 -0
- package/src/modes/interactive/components/footer.ts +324 -0
- package/src/modes/interactive/components/hook-selector.ts +3 -3
- package/src/modes/interactive/components/model-selector.ts +7 -6
- package/src/modes/interactive/components/oauth-selector.ts +3 -3
- package/src/modes/interactive/components/settings-defs.ts +55 -6
- package/src/modes/interactive/components/status-line.ts +45 -37
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +643 -113
- package/src/modes/interactive/theme/defaults/index.ts +16 -16
- package/src/modes/print-mode.ts +14 -72
- package/src/modes/rpc/rpc-client.ts +23 -9
- package/src/modes/rpc/rpc-mode.ts +137 -125
- package/src/modes/rpc/rpc-types.ts +46 -24
- package/src/prompts/task.md +1 -0
- package/src/prompts/tools/gemini-image.md +4 -0
- package/src/prompts/tools/git.md +9 -0
- package/src/prompts/voice-summary.md +12 -0
- package/src/utils/image-convert.ts +26 -0
- package/src/utils/image-resize.ts +215 -0
- package/src/utils/shell-snapshot.ts +22 -20
package/src/core/auth-storage.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Handles loading, saving, and refreshing credentials from auth.json.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { chmodSync, existsSync,
|
|
6
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { dirname } from "node:path";
|
|
8
8
|
import {
|
|
9
9
|
getEnvApiKey,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
loginAntigravity,
|
|
13
13
|
loginGeminiCli,
|
|
14
14
|
loginGitHubCopilot,
|
|
15
|
+
loginOpenAICodex,
|
|
15
16
|
type OAuthCredentials,
|
|
16
17
|
type OAuthProvider,
|
|
17
18
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -46,9 +47,7 @@ export class AuthStorage {
|
|
|
46
47
|
constructor(
|
|
47
48
|
private authPath: string,
|
|
48
49
|
private fallbackPaths: string[] = [],
|
|
49
|
-
) {
|
|
50
|
-
this.reload();
|
|
51
|
-
}
|
|
50
|
+
) {}
|
|
52
51
|
|
|
53
52
|
/**
|
|
54
53
|
* Set a runtime API key override (not persisted to disk).
|
|
@@ -77,7 +76,7 @@ export class AuthStorage {
|
|
|
77
76
|
* Reload credentials from disk.
|
|
78
77
|
* Checks primary path first, then fallback paths.
|
|
79
78
|
*/
|
|
80
|
-
reload(): void {
|
|
79
|
+
async reload(): Promise<void> {
|
|
81
80
|
const pathsToCheck = [this.authPath, ...this.fallbackPaths];
|
|
82
81
|
|
|
83
82
|
logger.debug("AuthStorage.reload checking paths", { paths: pathsToCheck });
|
|
@@ -105,13 +104,11 @@ export class AuthStorage {
|
|
|
105
104
|
/**
|
|
106
105
|
* Save credentials to disk.
|
|
107
106
|
*/
|
|
108
|
-
private save(): void {
|
|
109
|
-
|
|
110
|
-
if (!existsSync(dir)) {
|
|
111
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
112
|
-
}
|
|
113
|
-
Bun.write(this.authPath, JSON.stringify(this.data, null, 2));
|
|
107
|
+
private async save(): Promise<void> {
|
|
108
|
+
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2));
|
|
114
109
|
chmodSync(this.authPath, 0o600);
|
|
110
|
+
const dir = dirname(this.authPath);
|
|
111
|
+
chmodSync(dir, 0o700);
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
/**
|
|
@@ -124,17 +121,17 @@ export class AuthStorage {
|
|
|
124
121
|
/**
|
|
125
122
|
* Set credential for a provider.
|
|
126
123
|
*/
|
|
127
|
-
set(provider: string, credential: AuthCredential): void {
|
|
124
|
+
async set(provider: string, credential: AuthCredential): Promise<void> {
|
|
128
125
|
this.data[provider] = credential;
|
|
129
|
-
this.save();
|
|
126
|
+
await this.save();
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
/**
|
|
133
130
|
* Remove credential for a provider.
|
|
134
131
|
*/
|
|
135
|
-
remove(provider: string): void {
|
|
132
|
+
async remove(provider: string): Promise<void> {
|
|
136
133
|
delete this.data[provider];
|
|
137
|
-
this.save();
|
|
134
|
+
await this.save();
|
|
138
135
|
}
|
|
139
136
|
|
|
140
137
|
/**
|
|
@@ -145,12 +142,24 @@ export class AuthStorage {
|
|
|
145
142
|
}
|
|
146
143
|
|
|
147
144
|
/**
|
|
148
|
-
* Check if credentials exist for a provider.
|
|
145
|
+
* Check if credentials exist for a provider in auth.json.
|
|
149
146
|
*/
|
|
150
147
|
has(provider: string): boolean {
|
|
151
148
|
return provider in this.data;
|
|
152
149
|
}
|
|
153
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Check if any form of auth is configured for a provider.
|
|
153
|
+
* Unlike getApiKey(), this doesn't refresh OAuth tokens.
|
|
154
|
+
*/
|
|
155
|
+
hasAuth(provider: string): boolean {
|
|
156
|
+
if (this.runtimeOverrides.has(provider)) return true;
|
|
157
|
+
if (this.data[provider]) return true;
|
|
158
|
+
if (getEnvApiKey(provider)) return true;
|
|
159
|
+
if (this.fallbackResolver?.(provider)) return true;
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
/**
|
|
155
164
|
* Get all credentials (for passing to getOAuthApiKey).
|
|
156
165
|
*/
|
|
@@ -191,18 +200,21 @@ export class AuthStorage {
|
|
|
191
200
|
case "google-antigravity":
|
|
192
201
|
credentials = await loginAntigravity(callbacks.onAuth, callbacks.onProgress);
|
|
193
202
|
break;
|
|
203
|
+
case "openai-codex":
|
|
204
|
+
credentials = await loginOpenAICodex(callbacks);
|
|
205
|
+
break;
|
|
194
206
|
default:
|
|
195
207
|
throw new Error(`Unknown OAuth provider: ${provider}`);
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
this.set(provider, { type: "oauth", ...credentials });
|
|
210
|
+
await this.set(provider, { type: "oauth", ...credentials });
|
|
199
211
|
}
|
|
200
212
|
|
|
201
213
|
/**
|
|
202
214
|
* Logout from a provider.
|
|
203
215
|
*/
|
|
204
|
-
logout(provider: string): void {
|
|
205
|
-
this.remove(provider);
|
|
216
|
+
async logout(provider: string): Promise<void> {
|
|
217
|
+
await this.remove(provider);
|
|
206
218
|
}
|
|
207
219
|
|
|
208
220
|
/**
|
|
@@ -240,11 +252,11 @@ export class AuthStorage {
|
|
|
240
252
|
const result = await getOAuthApiKey(provider as OAuthProvider, oauthCreds);
|
|
241
253
|
if (result) {
|
|
242
254
|
this.data[provider] = { type: "oauth", ...result.newCredentials };
|
|
243
|
-
this.save();
|
|
255
|
+
await this.save();
|
|
244
256
|
return result.apiKey;
|
|
245
257
|
}
|
|
246
258
|
} catch {
|
|
247
|
-
this.remove(provider);
|
|
259
|
+
await this.remove(provider);
|
|
248
260
|
}
|
|
249
261
|
}
|
|
250
262
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
convertToLlm,
|
|
15
15
|
createBranchSummaryMessage,
|
|
16
16
|
createCompactionSummaryMessage,
|
|
17
|
-
|
|
17
|
+
createCustomMessage,
|
|
18
18
|
} from "../messages";
|
|
19
19
|
import type { ReadonlySessionManager, SessionEntry } from "../session-manager";
|
|
20
20
|
import { estimateTokens } from "./compaction";
|
|
@@ -149,7 +149,7 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
149
149
|
return entry.message;
|
|
150
150
|
|
|
151
151
|
case "custom_message":
|
|
152
|
-
return
|
|
152
|
+
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
|
|
153
153
|
|
|
154
154
|
case "branch_summary":
|
|
155
155
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
@@ -186,9 +186,9 @@ export function prepareBranchEntries(entries: SessionEntry[], tokenBudget: numbe
|
|
|
186
186
|
|
|
187
187
|
// First pass: collect file ops from ALL entries (even if they don't fit in token budget)
|
|
188
188
|
// This ensures we capture cumulative file tracking from nested branch summaries
|
|
189
|
-
// Only extract from pi-generated summaries (
|
|
189
|
+
// Only extract from pi-generated summaries (fromExtension !== true), not extension-generated ones
|
|
190
190
|
for (const entry of entries) {
|
|
191
|
-
if (entry.type === "branch_summary" && !entry.
|
|
191
|
+
if (entry.type === "branch_summary" && !entry.fromExtension && entry.details) {
|
|
192
192
|
const details = entry.details as BranchSummaryDetails;
|
|
193
193
|
if (Array.isArray(details.readFiles)) {
|
|
194
194
|
for (const f of details.readFiles) fileOps.read.add(f);
|
|
@@ -11,7 +11,7 @@ import { complete, completeSimple } from "@oh-my-pi/pi-ai";
|
|
|
11
11
|
import compactionSummaryPrompt from "../../prompts/compaction-summary.md" with { type: "text" };
|
|
12
12
|
import compactionTurnPrefixPrompt from "../../prompts/compaction-turn-prefix.md" with { type: "text" };
|
|
13
13
|
import compactionUpdateSummaryPrompt from "../../prompts/compaction-update-summary.md" with { type: "text" };
|
|
14
|
-
import { convertToLlm, createBranchSummaryMessage,
|
|
14
|
+
import { convertToLlm, createBranchSummaryMessage, createCustomMessage } from "../messages";
|
|
15
15
|
import type { CompactionEntry, SessionEntry } from "../session-manager";
|
|
16
16
|
import {
|
|
17
17
|
computeFileLists,
|
|
@@ -46,7 +46,7 @@ function extractFileOperations(
|
|
|
46
46
|
// Collect from previous compaction's details (if pi-generated)
|
|
47
47
|
if (prevCompactionIndex >= 0) {
|
|
48
48
|
const prevCompaction = entries[prevCompactionIndex] as CompactionEntry;
|
|
49
|
-
if (!prevCompaction.
|
|
49
|
+
if (!prevCompaction.fromExtension && prevCompaction.details) {
|
|
50
50
|
const details = prevCompaction.details as CompactionDetails;
|
|
51
51
|
if (Array.isArray(details.readFiles)) {
|
|
52
52
|
for (const f of details.readFiles) fileOps.read.add(f);
|
|
@@ -78,7 +78,7 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
78
78
|
return entry.message;
|
|
79
79
|
}
|
|
80
80
|
if (entry.type === "custom_message") {
|
|
81
|
-
return
|
|
81
|
+
return createCustomMessage(entry.customType, entry.content, entry.display, entry.details, entry.timestamp);
|
|
82
82
|
}
|
|
83
83
|
if (entry.type === "branch_summary") {
|
|
84
84
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { type CollapseStrategy, collapse } from "../../../../lib/worktree/collapse";
|
|
4
|
+
import { WorktreeError, WorktreeErrorCode } from "../../../../lib/worktree/errors";
|
|
5
|
+
import { getRepoRoot, git } from "../../../../lib/worktree/git";
|
|
6
|
+
import * as worktree from "../../../../lib/worktree/index";
|
|
7
|
+
import { createSession, updateSession } from "../../../../lib/worktree/session";
|
|
8
|
+
import { formatStats, getStats } from "../../../../lib/worktree/stats";
|
|
9
|
+
import type { HookCommandContext } from "../../../hooks/types";
|
|
10
|
+
import { discoverAgents, getAgent } from "../../../tools/task/discovery";
|
|
11
|
+
import { runSubprocess } from "../../../tools/task/executor";
|
|
12
|
+
import type { AgentDefinition } from "../../../tools/task/types";
|
|
13
|
+
import type { CustomCommand, CustomCommandAPI } from "../../types";
|
|
14
|
+
|
|
15
|
+
interface FlagParseResult {
|
|
16
|
+
positionals: string[];
|
|
17
|
+
flags: Map<string, string | boolean>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface NewArgs {
|
|
21
|
+
branch: string;
|
|
22
|
+
base?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface MergeArgs {
|
|
26
|
+
source: string;
|
|
27
|
+
target?: string;
|
|
28
|
+
strategy?: CollapseStrategy;
|
|
29
|
+
keep?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RmArgs {
|
|
33
|
+
name: string;
|
|
34
|
+
force?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface SpawnArgs {
|
|
38
|
+
task: string;
|
|
39
|
+
scope?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ParallelTask {
|
|
44
|
+
task: string;
|
|
45
|
+
scope: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseFlags(args: string[]): FlagParseResult {
|
|
49
|
+
const flags = new Map<string, string | boolean>();
|
|
50
|
+
const positionals: string[] = [];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < args.length; i++) {
|
|
53
|
+
const arg = args[i];
|
|
54
|
+
if (arg.startsWith("--")) {
|
|
55
|
+
const name = arg.slice(2);
|
|
56
|
+
const next = args[i + 1];
|
|
57
|
+
if (next && !next.startsWith("--")) {
|
|
58
|
+
flags.set(name, next);
|
|
59
|
+
i += 1;
|
|
60
|
+
} else {
|
|
61
|
+
flags.set(name, true);
|
|
62
|
+
}
|
|
63
|
+
} else {
|
|
64
|
+
positionals.push(arg);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { positionals, flags };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getFlagValue(flags: Map<string, string | boolean>, name: string): string | undefined {
|
|
72
|
+
const value = flags.get(name);
|
|
73
|
+
if (typeof value === "string") return value;
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getFlagBoolean(flags: Map<string, string | boolean>, name: string): boolean {
|
|
78
|
+
return flags.get(name) === true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatUsage(): string {
|
|
82
|
+
return [
|
|
83
|
+
"Usage:",
|
|
84
|
+
" /wt new <branch> [--base <ref>]",
|
|
85
|
+
" /wt list",
|
|
86
|
+
" /wt merge <src> [dst] [--strategy simple|merge-base|rebase] [--keep]",
|
|
87
|
+
" /wt rm <name> [--force]",
|
|
88
|
+
" /wt status",
|
|
89
|
+
' /wt spawn "<task>" [--scope <glob>] [--name <branch>]',
|
|
90
|
+
" /wt parallel --task <t> --scope <s> [--task <t> --scope <s>]...",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatError(err: unknown): string {
|
|
95
|
+
if (err instanceof WorktreeError) {
|
|
96
|
+
return `${err.code}: ${err.message}`;
|
|
97
|
+
}
|
|
98
|
+
if (err instanceof Error) return err.message;
|
|
99
|
+
return String(err);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function pickAgent(cwd: string): Promise<AgentDefinition> {
|
|
103
|
+
const { agents } = await discoverAgents(cwd);
|
|
104
|
+
// Use the bundled "task" agent as the general-purpose default.
|
|
105
|
+
const agent = getAgent(agents, "task") ?? agents[0];
|
|
106
|
+
if (!agent) {
|
|
107
|
+
throw new Error("No agents available");
|
|
108
|
+
}
|
|
109
|
+
return agent;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function parseParallelTasks(args: string[]): ParallelTask[] {
|
|
113
|
+
const tasks: ParallelTask[] = [];
|
|
114
|
+
let current: Partial<ParallelTask> = {};
|
|
115
|
+
|
|
116
|
+
for (let i = 0; i < args.length; i++) {
|
|
117
|
+
const arg = args[i];
|
|
118
|
+
if (arg === "--task") {
|
|
119
|
+
const value = args[i + 1];
|
|
120
|
+
if (!value || value.startsWith("--")) {
|
|
121
|
+
throw new Error("Missing value for --task");
|
|
122
|
+
}
|
|
123
|
+
current.task = value;
|
|
124
|
+
i += 1;
|
|
125
|
+
} else if (arg === "--scope") {
|
|
126
|
+
const value = args[i + 1];
|
|
127
|
+
if (!value || value.startsWith("--")) {
|
|
128
|
+
throw new Error("Missing value for --scope");
|
|
129
|
+
}
|
|
130
|
+
current.scope = value;
|
|
131
|
+
i += 1;
|
|
132
|
+
} else {
|
|
133
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (current.task && current.scope) {
|
|
137
|
+
tasks.push({ task: current.task, scope: current.scope });
|
|
138
|
+
current = {};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (current.task || current.scope) {
|
|
143
|
+
throw new Error("Each --task must be paired with a --scope");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return tasks;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateDisjointScopes(scopes: string[]): void {
|
|
150
|
+
for (let i = 0; i < scopes.length; i++) {
|
|
151
|
+
for (let j = i + 1; j < scopes.length; j++) {
|
|
152
|
+
const a = scopes[i].replace(/\*.*$/, "");
|
|
153
|
+
const b = scopes[j].replace(/\*.*$/, "");
|
|
154
|
+
if (a.startsWith(b) || b.startsWith(a)) {
|
|
155
|
+
throw new WorktreeError(
|
|
156
|
+
`Overlapping scopes: "${scopes[i]}" and "${scopes[j]}"`,
|
|
157
|
+
WorktreeErrorCode.OVERLAPPING_SCOPES,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handleNew(args: NewArgs): Promise<string> {
|
|
165
|
+
const wt = await worktree.create(args.branch, { base: args.base });
|
|
166
|
+
|
|
167
|
+
return [`Created worktree: ${wt.path}`, `Branch: ${wt.branch ?? "detached"}`, "", `To switch: cd ${wt.path}`].join(
|
|
168
|
+
"\n",
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function handleList(ctx: HookCommandContext): Promise<string> {
|
|
173
|
+
const worktrees = await worktree.list();
|
|
174
|
+
const cwd = path.resolve(ctx.cwd);
|
|
175
|
+
const mainPath = await getRepoRoot();
|
|
176
|
+
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
|
|
179
|
+
for (const wt of worktrees) {
|
|
180
|
+
const stats = await getStats(wt.path);
|
|
181
|
+
const isCurrent = cwd === wt.path || cwd.startsWith(wt.path + path.sep);
|
|
182
|
+
const isMain = wt.path === mainPath;
|
|
183
|
+
|
|
184
|
+
const marker = isCurrent ? "->" : " ";
|
|
185
|
+
const mainTag = isMain ? " [main]" : "";
|
|
186
|
+
const branch = wt.branch ?? "detached";
|
|
187
|
+
const statsStr = formatStats(stats);
|
|
188
|
+
|
|
189
|
+
lines.push(`${marker} ${branch}${mainTag} (${statsStr})`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return lines.join("\n") || "No worktrees found";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function handleMerge(args: MergeArgs): Promise<string> {
|
|
196
|
+
const target = args.target ?? "main";
|
|
197
|
+
const strategy = args.strategy ?? "rebase";
|
|
198
|
+
|
|
199
|
+
const result = await collapse(args.source, target, {
|
|
200
|
+
strategy,
|
|
201
|
+
keepSource: args.keep,
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const lines = [
|
|
205
|
+
`Collapsed ${args.source} -> ${target}`,
|
|
206
|
+
`Strategy: ${strategy}`,
|
|
207
|
+
`Changes: +${result.insertions} -${result.deletions} in ${result.filesChanged} files`,
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
if (!args.keep) {
|
|
211
|
+
lines.push("Source worktree removed");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function handleRm(args: RmArgs): Promise<string> {
|
|
218
|
+
const wt = await worktree.find(args.name);
|
|
219
|
+
await worktree.remove(args.name, { force: args.force });
|
|
220
|
+
|
|
221
|
+
const mainPath = await getRepoRoot();
|
|
222
|
+
if (wt.branch) {
|
|
223
|
+
await git(["branch", "-D", wt.branch], mainPath);
|
|
224
|
+
return `Removed worktree and branch: ${wt.branch}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return `Removed worktree: ${wt.path}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function handleStatus(): Promise<string> {
|
|
231
|
+
const worktrees = await worktree.list();
|
|
232
|
+
const sections: string[] = [];
|
|
233
|
+
|
|
234
|
+
for (const wt of worktrees) {
|
|
235
|
+
const branch = wt.branch ?? "detached";
|
|
236
|
+
const name = path.basename(wt.path);
|
|
237
|
+
|
|
238
|
+
const statusResult = await git(["status", "--short"], wt.path);
|
|
239
|
+
const status = statusResult.stdout.trim() || "(clean)";
|
|
240
|
+
|
|
241
|
+
sections.push(`${name} (${branch})\n${"-".repeat(40)}\n${status}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return sections.join("\n\n");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function handleSpawn(args: SpawnArgs, ctx: HookCommandContext): Promise<string> {
|
|
248
|
+
const branch = args.name ?? `wt-agent-${nanoid(6)}`;
|
|
249
|
+
const wt = await worktree.create(branch);
|
|
250
|
+
|
|
251
|
+
const session = await createSession({
|
|
252
|
+
branch,
|
|
253
|
+
path: wt.path,
|
|
254
|
+
scope: args.scope ? [args.scope] : undefined,
|
|
255
|
+
task: args.task,
|
|
256
|
+
});
|
|
257
|
+
await updateSession(session.id, { status: "active" });
|
|
258
|
+
|
|
259
|
+
const agent = await pickAgent(ctx.cwd);
|
|
260
|
+
const context = args.scope ? `Scope: ${args.scope}` : undefined;
|
|
261
|
+
|
|
262
|
+
// Command context doesn't expose a spawn API, so run the task subprocess directly.
|
|
263
|
+
const result = await runSubprocess({
|
|
264
|
+
cwd: wt.path,
|
|
265
|
+
agent,
|
|
266
|
+
task: args.task,
|
|
267
|
+
index: 0,
|
|
268
|
+
context,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await updateSession(session.id, {
|
|
272
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
273
|
+
completedAt: Date.now(),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (result.exitCode !== 0) {
|
|
277
|
+
return [
|
|
278
|
+
`Agent failed in worktree: ${branch}`,
|
|
279
|
+
result.stderr.trim() ? `Error: ${result.stderr.trim()}` : "Error: agent execution failed",
|
|
280
|
+
"",
|
|
281
|
+
"Actions:",
|
|
282
|
+
` /wt merge ${branch} - Apply changes to main`,
|
|
283
|
+
" /wt status - Inspect changes",
|
|
284
|
+
` /wt rm ${branch} - Discard changes`,
|
|
285
|
+
].join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return [
|
|
289
|
+
`Agent completed in worktree: ${branch}`,
|
|
290
|
+
"",
|
|
291
|
+
"Actions:",
|
|
292
|
+
` /wt merge ${branch} - Apply changes to main`,
|
|
293
|
+
" /wt status - Inspect changes",
|
|
294
|
+
` /wt rm ${branch} - Discard changes`,
|
|
295
|
+
].join("\n");
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function handleParallel(args: ParallelTask[], ctx: HookCommandContext): Promise<string> {
|
|
299
|
+
validateDisjointScopes(args.map((t) => t.scope));
|
|
300
|
+
|
|
301
|
+
const sessionId = `parallel-${Date.now()}`;
|
|
302
|
+
const agent = await pickAgent(ctx.cwd);
|
|
303
|
+
|
|
304
|
+
const worktrees: Array<{ task: ParallelTask; wt: worktree.Worktree; session: worktree.WorktreeSession }> = [];
|
|
305
|
+
for (let i = 0; i < args.length; i++) {
|
|
306
|
+
const task = args[i];
|
|
307
|
+
const branch = `wt-parallel-${sessionId}-${i}`;
|
|
308
|
+
const wt = await worktree.create(branch);
|
|
309
|
+
const session = await createSession({
|
|
310
|
+
branch,
|
|
311
|
+
path: wt.path,
|
|
312
|
+
scope: [task.scope],
|
|
313
|
+
task: task.task,
|
|
314
|
+
});
|
|
315
|
+
worktrees.push({ task, wt, session });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const agentPromises = worktrees.map(async ({ task, wt, session }, index) => {
|
|
319
|
+
await updateSession(session.id, { status: "active" });
|
|
320
|
+
const result = await runSubprocess({
|
|
321
|
+
cwd: wt.path,
|
|
322
|
+
agent,
|
|
323
|
+
task: task.task,
|
|
324
|
+
index,
|
|
325
|
+
context: `Scope: ${task.scope}`,
|
|
326
|
+
});
|
|
327
|
+
await updateSession(session.id, {
|
|
328
|
+
status: result.exitCode === 0 ? "completed" : "failed",
|
|
329
|
+
completedAt: Date.now(),
|
|
330
|
+
});
|
|
331
|
+
return { wt, session, result };
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const results = await Promise.all(agentPromises);
|
|
335
|
+
|
|
336
|
+
const mergeResults: string[] = [];
|
|
337
|
+
|
|
338
|
+
for (const { wt, session } of results) {
|
|
339
|
+
try {
|
|
340
|
+
await updateSession(session.id, { status: "merging" });
|
|
341
|
+
const collapseResult = await collapse(wt.branch ?? wt.path, "main", {
|
|
342
|
+
strategy: "simple",
|
|
343
|
+
keepSource: false,
|
|
344
|
+
});
|
|
345
|
+
await updateSession(session.id, { status: "merged" });
|
|
346
|
+
mergeResults.push(
|
|
347
|
+
`ok ${wt.branch ?? path.basename(wt.path)}: +${collapseResult.insertions} -${collapseResult.deletions}`,
|
|
348
|
+
);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
await updateSession(session.id, { status: "failed" });
|
|
351
|
+
mergeResults.push(`err ${wt.branch ?? path.basename(wt.path)}: ${formatError(err)}`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return [`Parallel execution complete (${args.length} agents)`, "", "Results:", ...mergeResults].join("\n");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export function createWorktreeCommand(_api: CustomCommandAPI): CustomCommand {
|
|
359
|
+
return {
|
|
360
|
+
name: "wt",
|
|
361
|
+
description: "Git worktree management",
|
|
362
|
+
async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
|
|
363
|
+
if (args.length === 0) return formatUsage();
|
|
364
|
+
|
|
365
|
+
const subcommand = args[0];
|
|
366
|
+
const rest = args.slice(1);
|
|
367
|
+
|
|
368
|
+
try {
|
|
369
|
+
switch (subcommand) {
|
|
370
|
+
case "new": {
|
|
371
|
+
const parsed = parseFlags(rest);
|
|
372
|
+
const branch = parsed.positionals[0];
|
|
373
|
+
if (!branch) return formatUsage();
|
|
374
|
+
const base = getFlagValue(parsed.flags, "base");
|
|
375
|
+
if (parsed.flags.get("base") === true) {
|
|
376
|
+
return "Missing value for --base";
|
|
377
|
+
}
|
|
378
|
+
return await handleNew({ branch, base });
|
|
379
|
+
}
|
|
380
|
+
case "list":
|
|
381
|
+
return await handleList(ctx);
|
|
382
|
+
case "merge": {
|
|
383
|
+
const parsed = parseFlags(rest);
|
|
384
|
+
const source = parsed.positionals[0];
|
|
385
|
+
const target = parsed.positionals[1];
|
|
386
|
+
if (!source) return formatUsage();
|
|
387
|
+
const strategyRaw = getFlagValue(parsed.flags, "strategy");
|
|
388
|
+
if (parsed.flags.get("strategy") === true) {
|
|
389
|
+
return "Missing value for --strategy";
|
|
390
|
+
}
|
|
391
|
+
const strategy = strategyRaw as CollapseStrategy | undefined;
|
|
392
|
+
const keep = getFlagBoolean(parsed.flags, "keep");
|
|
393
|
+
return await handleMerge({ source, target, strategy, keep });
|
|
394
|
+
}
|
|
395
|
+
case "rm": {
|
|
396
|
+
const parsed = parseFlags(rest);
|
|
397
|
+
const name = parsed.positionals[0];
|
|
398
|
+
if (!name) return formatUsage();
|
|
399
|
+
const force = getFlagBoolean(parsed.flags, "force");
|
|
400
|
+
return await handleRm({ name, force });
|
|
401
|
+
}
|
|
402
|
+
case "status":
|
|
403
|
+
return await handleStatus();
|
|
404
|
+
case "spawn": {
|
|
405
|
+
const parsed = parseFlags(rest);
|
|
406
|
+
const task = parsed.positionals[0];
|
|
407
|
+
if (!task) return formatUsage();
|
|
408
|
+
const scope = getFlagValue(parsed.flags, "scope");
|
|
409
|
+
if (parsed.flags.get("scope") === true) {
|
|
410
|
+
return "Missing value for --scope";
|
|
411
|
+
}
|
|
412
|
+
const name = getFlagValue(parsed.flags, "name");
|
|
413
|
+
return await handleSpawn({ task, scope, name }, ctx);
|
|
414
|
+
}
|
|
415
|
+
case "parallel": {
|
|
416
|
+
const tasks = parseParallelTasks(rest);
|
|
417
|
+
if (tasks.length === 0) return formatUsage();
|
|
418
|
+
return await handleParallel(tasks, ctx);
|
|
419
|
+
}
|
|
420
|
+
default:
|
|
421
|
+
return formatUsage();
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
return formatError(err);
|
|
425
|
+
}
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export default createWorktreeCommand;
|
|
@@ -12,6 +12,7 @@ import { getAgentDir, getConfigDirs } from "../../config";
|
|
|
12
12
|
import * as piCodingAgent from "../../index";
|
|
13
13
|
import { execCommand } from "../exec";
|
|
14
14
|
import { createReviewCommand } from "./bundled/review";
|
|
15
|
+
import { createWorktreeCommand } from "./bundled/wt";
|
|
15
16
|
import type {
|
|
16
17
|
CustomCommand,
|
|
17
18
|
CustomCommandAPI,
|
|
@@ -151,6 +152,14 @@ function loadBundledCommands(sharedApi: CustomCommandAPI): LoadedCustomCommand[]
|
|
|
151
152
|
source: "bundled",
|
|
152
153
|
});
|
|
153
154
|
|
|
155
|
+
const worktreeCommand = createWorktreeCommand(sharedApi);
|
|
156
|
+
bundled.push({
|
|
157
|
+
path: "bundled:wt",
|
|
158
|
+
resolvedPath: "bundled:wt",
|
|
159
|
+
command: worktreeCommand,
|
|
160
|
+
source: "bundled",
|
|
161
|
+
});
|
|
162
|
+
|
|
154
163
|
return bundled;
|
|
155
164
|
}
|
|
156
165
|
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
6
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
6
7
|
import type { CustomTool, CustomToolContext, LoadedCustomTool } from "./types";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -18,6 +19,10 @@ export function wrapCustomTool(tool: CustomTool, getContext: () => CustomToolCon
|
|
|
18
19
|
hidden: tool.hidden,
|
|
19
20
|
execute: (toolCallId, params, signal, onUpdate, context) =>
|
|
20
21
|
tool.execute(toolCallId, params, onUpdate, context ?? getContext(), signal),
|
|
22
|
+
renderCall: tool.renderCall ? (args, theme) => tool.renderCall?.(args, theme as Theme) : undefined,
|
|
23
|
+
renderResult: tool.renderResult
|
|
24
|
+
? (result, options, theme) => tool.renderResult?.(result, options, theme as Theme)
|
|
25
|
+
: undefined,
|
|
21
26
|
};
|
|
22
27
|
}
|
|
23
28
|
|