@oh-my-pi/pi-coding-agent 15.8.0 → 15.8.2
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 +26 -0
- package/dist/types/config/keybindings.d.ts +5 -1
- package/dist/types/edit/index.d.ts +6 -0
- package/dist/types/edit/streaming.d.ts +8 -0
- package/dist/types/export/ttsr.d.ts +9 -0
- package/dist/types/mcp/transports/stdio.d.ts +19 -0
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/utils/jj.d.ts +49 -0
- package/package.json +9 -9
- package/src/config/keybindings.ts +8 -1
- package/src/config/model-registry.ts +18 -7
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-deprecated-leftovers.md +44 -0
- package/src/edit/index.ts +10 -0
- package/src/edit/streaming.ts +65 -0
- package/src/export/ttsr.ts +18 -1
- package/src/extensibility/custom-commands/bundled/review/index.ts +74 -45
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/mcp/transports/stdio.ts +55 -22
- package/src/prompts/agents/reviewer.md +2 -2
- package/src/prompts/review-request.md +1 -1
- package/src/prompts/system/empty-stop-retry.md +6 -0
- package/src/prompts/tools/search-tool-bm25.md +9 -2
- package/src/session/agent-session.ts +147 -8
- package/src/tools/search-tool-bm25.ts +7 -1
- package/src/tools/write.ts +6 -0
- package/src/utils/git.ts +19 -23
- package/src/utils/jj.ts +225 -0
package/src/utils/jj.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { $which } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { LRUCache } from "lru-cache/raw";
|
|
5
|
+
|
|
6
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
7
|
+
// Types
|
|
8
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
9
|
+
|
|
10
|
+
/** Result from a completed `jj` subprocess invocation. */
|
|
11
|
+
export interface JjCommandResult {
|
|
12
|
+
/** Process exit code reported by `jj`. */
|
|
13
|
+
exitCode: number;
|
|
14
|
+
/** Captured standard output as UTF-8 text. */
|
|
15
|
+
stdout: string;
|
|
16
|
+
/** Captured standard error as UTF-8 text. */
|
|
17
|
+
stderr: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Resolved Jujutsu workspace metadata. */
|
|
21
|
+
export interface JjRepository {
|
|
22
|
+
/** Root directory containing the `.jj` workspace metadata. */
|
|
23
|
+
repoRoot: string;
|
|
24
|
+
/** Path to the workspace store directory used to verify a real JJ checkout. */
|
|
25
|
+
storeDir: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Options for `jj diff` invocations. */
|
|
29
|
+
export interface DiffOptions {
|
|
30
|
+
/** Optional file paths to restrict the diff with `-- <files>`. */
|
|
31
|
+
readonly files?: readonly string[];
|
|
32
|
+
/** Return only changed file names instead of Git-format diff text. */
|
|
33
|
+
readonly nameOnly?: boolean;
|
|
34
|
+
/** Optional abort signal passed to the spawned `jj` process. */
|
|
35
|
+
readonly signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface CommandOptions {
|
|
39
|
+
readonly signal?: AbortSignal;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
43
|
+
// Error
|
|
44
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
|
|
46
|
+
/** Error thrown when a checked `jj` command exits non-zero. */
|
|
47
|
+
export class JjCommandError extends Error {
|
|
48
|
+
/** Arguments passed after the common `jj --no-pager --color=never` prefix. */
|
|
49
|
+
readonly args: readonly string[];
|
|
50
|
+
/** Captured command result that caused the failure. */
|
|
51
|
+
readonly result: JjCommandResult;
|
|
52
|
+
|
|
53
|
+
/** Create an error for a failed checked `jj` command. */
|
|
54
|
+
constructor(args: readonly string[], result: JjCommandResult) {
|
|
55
|
+
super(formatCommandFailure(args, result));
|
|
56
|
+
this.name = "JjCommandError";
|
|
57
|
+
this.args = [...args];
|
|
58
|
+
this.result = result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
63
|
+
// Internal: Core execution
|
|
64
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
65
|
+
|
|
66
|
+
function ensureAvailable(): void {
|
|
67
|
+
if (!$which("jj")) {
|
|
68
|
+
throw new Error("jj is not installed.");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function formatCommandFailure(
|
|
73
|
+
args: readonly string[],
|
|
74
|
+
result: Pick<JjCommandResult, "exitCode" | "stdout" | "stderr">,
|
|
75
|
+
): string {
|
|
76
|
+
const stderr = result.stderr.trim();
|
|
77
|
+
if (stderr) return stderr;
|
|
78
|
+
const stdout = result.stdout.trim();
|
|
79
|
+
if (stdout) return stdout;
|
|
80
|
+
return `jj ${args.join(" ")} failed with exit code ${result.exitCode}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function jj(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<JjCommandResult> {
|
|
84
|
+
const child = Bun.spawn(["jj", "--no-pager", "--color=never", ...args], {
|
|
85
|
+
cwd,
|
|
86
|
+
signal: options.signal,
|
|
87
|
+
stdin: "ignore",
|
|
88
|
+
stdout: "pipe",
|
|
89
|
+
stderr: "pipe",
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (!child.stdout || !child.stderr) {
|
|
94
|
+
throw new Error("Failed to capture jj command output.");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
98
|
+
new Response(child.stdout).text(),
|
|
99
|
+
new Response(child.stderr).text(),
|
|
100
|
+
child.exited,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
return { exitCode: exitCode ?? 0, stdout, stderr };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function runChecked(
|
|
107
|
+
cwd: string,
|
|
108
|
+
args: readonly string[],
|
|
109
|
+
options: CommandOptions = {},
|
|
110
|
+
): Promise<JjCommandResult> {
|
|
111
|
+
ensureAvailable();
|
|
112
|
+
const result = await jj(cwd, args, options);
|
|
113
|
+
if (result.exitCode !== 0) {
|
|
114
|
+
throw new JjCommandError(args, result);
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runText(cwd: string, args: readonly string[], options: CommandOptions = {}): Promise<string> {
|
|
120
|
+
return (await runChecked(cwd, args, options)).stdout;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function splitLines(text: string): string[] {
|
|
124
|
+
return text
|
|
125
|
+
.split("\n")
|
|
126
|
+
.map(line => line.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildDiffArgs(options: DiffOptions): string[] {
|
|
131
|
+
const args = ["diff"];
|
|
132
|
+
args.push(options.nameOnly ? "--name-only" : "--git");
|
|
133
|
+
if (options.files?.length) args.push("--", ...options.files);
|
|
134
|
+
return args;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
138
|
+
// Internal: Repository resolution
|
|
139
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
140
|
+
|
|
141
|
+
interface WorkspaceRootCacheEntry {
|
|
142
|
+
readonly root?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const WORKSPACE_ROOT_CACHE_MAX_ENTRIES = 256;
|
|
146
|
+
const workspaceRootCache = new LRUCache<string, WorkspaceRootCacheEntry>({ max: WORKSPACE_ROOT_CACHE_MAX_ENTRIES });
|
|
147
|
+
|
|
148
|
+
async function hasJjWorkspaceMetadata(dir: string): Promise<boolean> {
|
|
149
|
+
try {
|
|
150
|
+
return (await fs.stat(path.join(dir, ".jj", "repo", "store"))).isDirectory();
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function parentOf(dir: string): string | undefined {
|
|
157
|
+
const parent = path.dirname(dir);
|
|
158
|
+
return parent === dir ? undefined : parent;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function findWorkspaceRoot(cwd: string): Promise<string | undefined> {
|
|
162
|
+
const key = path.resolve(cwd);
|
|
163
|
+
if (workspaceRootCache.has(key)) return workspaceRootCache.get(key)?.root;
|
|
164
|
+
|
|
165
|
+
for (let dir: string | undefined = key; dir; dir = parentOf(dir)) {
|
|
166
|
+
if (await hasJjWorkspaceMetadata(dir)) {
|
|
167
|
+
workspaceRootCache.set(key, { root: dir });
|
|
168
|
+
return dir;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
workspaceRootCache.set(key, {});
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function repositoryFromRoot(root: string): JjRepository {
|
|
177
|
+
return {
|
|
178
|
+
repoRoot: root,
|
|
179
|
+
storeDir: path.join(root, ".jj", "repo", "store"),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
184
|
+
// API: diff
|
|
185
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
186
|
+
|
|
187
|
+
/** Run `jj diff --git` for the current workspace commit and return the raw Git-format diff text. */
|
|
188
|
+
export const diff = Object.assign(
|
|
189
|
+
async function diff(cwd: string, options: DiffOptions = {}): Promise<string> {
|
|
190
|
+
return runText(cwd, buildDiffArgs(options), { signal: options.signal });
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
/** List changed file paths. */
|
|
194
|
+
async changedFiles(cwd: string, options: Pick<DiffOptions, "files" | "signal"> = {}): Promise<string[]> {
|
|
195
|
+
return splitLines(await diff(cwd, { ...options, nameOnly: true }));
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
201
|
+
// API: repo
|
|
202
|
+
// ════════════════════════════════════════════════════════════════════════════
|
|
203
|
+
|
|
204
|
+
export const repo = {
|
|
205
|
+
/** Clear cached workspace roots. Intended for tests that mutate JJ metadata under an existing path. */
|
|
206
|
+
clearRootCache(): void {
|
|
207
|
+
workspaceRootCache.clear();
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/** Resolve the current Jujutsu workspace root, or `null` when `cwd` is not in a JJ repository. */
|
|
211
|
+
async root(cwd: string): Promise<string | null> {
|
|
212
|
+
return (await findWorkspaceRoot(cwd)) ?? null;
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/** Full Jujutsu workspace metadata. */
|
|
216
|
+
async resolve(cwd: string): Promise<JjRepository | null> {
|
|
217
|
+
const root = await repo.root(cwd);
|
|
218
|
+
return root ? repositoryFromRoot(root) : null;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/** Check whether `cwd` is inside a Jujutsu repository. */
|
|
222
|
+
async is(cwd: string): Promise<boolean> {
|
|
223
|
+
return (await repo.root(cwd)) !== null;
|
|
224
|
+
},
|
|
225
|
+
};
|