@oh-my-pi/pi-coding-agent 3.30.0 → 3.32.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 +85 -0
- package/package.json +5 -5
- package/src/cli/args.ts +4 -0
- package/src/core/agent-session.ts +29 -2
- package/src/core/bash-executor.ts +2 -1
- package/src/core/custom-commands/bundled/review/index.ts +367 -14
- package/src/core/custom-commands/bundled/wt/index.ts +1 -1
- package/src/core/sdk.ts +10 -2
- package/src/core/session-manager.ts +158 -246
- package/src/core/session-storage.ts +379 -0
- package/src/core/settings-manager.ts +155 -4
- package/src/core/slash-commands.ts +39 -13
- package/src/core/system-prompt.ts +62 -64
- package/src/core/tools/ask.ts +5 -4
- package/src/core/tools/bash-interceptor.ts +26 -61
- package/src/core/tools/bash.ts +13 -8
- package/src/core/tools/edit-diff.ts +11 -4
- package/src/core/tools/edit.ts +7 -13
- package/src/core/tools/find.ts +111 -50
- package/src/core/tools/gemini-image.ts +128 -147
- package/src/core/tools/grep.ts +397 -415
- package/src/core/tools/index.test.ts +5 -1
- package/src/core/tools/index.ts +8 -4
- package/src/core/tools/ls.ts +12 -10
- package/src/core/tools/lsp/client.ts +84 -19
- package/src/core/tools/lsp/config.ts +205 -656
- package/src/core/tools/lsp/defaults.json +465 -0
- package/src/core/tools/lsp/index.ts +72 -35
- package/src/core/tools/lsp/rust-analyzer.ts +49 -10
- package/src/core/tools/lsp/types.ts +1 -0
- package/src/core/tools/lsp/utils.ts +1 -1
- package/src/core/tools/read.ts +150 -74
- package/src/core/tools/render-utils.ts +70 -10
- package/src/core/tools/review.ts +38 -126
- package/src/core/tools/task/artifacts.ts +5 -4
- package/src/core/tools/task/commands.ts +4 -0
- package/src/core/tools/task/executor.ts +94 -83
- package/src/core/tools/task/index.ts +130 -92
- package/src/core/tools/task/parallel.ts +30 -3
- package/src/core/tools/task/render.ts +85 -39
- package/src/core/tools/task/types.ts +15 -6
- package/src/core/tools/task/worker.ts +124 -89
- package/src/core/tools/web-fetch.ts +112 -377
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/artifacthub.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/arxiv.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/aur.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/biorxiv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/bluesky.ts +10 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/brew.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/cheatsh.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/chocolatey.ts +6 -1
- package/src/core/tools/web-scrapers/choosealicense.ts +110 -0
- package/src/core/tools/web-scrapers/cisa-kev.ts +100 -0
- package/src/core/tools/web-scrapers/clojars.ts +180 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/coingecko.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/crates-io.ts +7 -2
- package/src/core/tools/web-scrapers/crossref.ts +149 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/devto.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/discogs.ts +6 -1
- package/src/core/tools/web-scrapers/discourse.ts +221 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/dockerhub.ts +7 -3
- package/src/core/tools/web-scrapers/fdroid.ts +158 -0
- package/src/core/tools/web-scrapers/firefox-addons.ts +214 -0
- package/src/core/tools/web-scrapers/flathub.ts +239 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github-gist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/github.ts +63 -32
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/gitlab.ts +31 -19
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/go-pkg.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackage.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hackernews.ts +18 -18
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/hex.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/huggingface.ts +10 -10
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/iacr.ts +8 -4
- package/src/core/tools/web-scrapers/index.ts +250 -0
- package/src/core/tools/web-scrapers/jetbrains-marketplace.ts +169 -0
- package/src/core/tools/web-scrapers/lemmy.ts +220 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/lobsters.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mastodon.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/maven.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/mdn.ts +2 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/metacpan.ts +13 -7
- package/src/core/tools/web-scrapers/musicbrainz.ts +273 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/npm.ts +12 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nuget.ts +9 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/nvd.ts +6 -1
- package/src/core/tools/web-scrapers/ollama.ts +267 -0
- package/src/core/tools/web-scrapers/open-vsx.ts +119 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/opencorporates.ts +2 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/openlibrary.ts +18 -12
- package/src/core/tools/web-scrapers/orcid.ts +299 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/osv.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/packagist.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pub-dev.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pubmed.ts +8 -4
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/pypi.ts +7 -3
- package/src/core/tools/web-scrapers/rawg.ts +124 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/readthedocs.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/reddit.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/repology.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rfc.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/rubygems.ts +6 -1
- package/src/core/tools/web-scrapers/searchcode.ts +217 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/sec-edgar.ts +6 -1
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/semantic-scholar.ts +2 -2
- package/src/core/tools/web-scrapers/snapcraft.ts +200 -0
- package/src/core/tools/web-scrapers/sourcegraph.ts +373 -0
- package/src/core/tools/web-scrapers/spdx.ts +121 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/spotify.ts +3 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackoverflow.ts +3 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/terraform.ts +11 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/tldr.ts +6 -2
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/twitter.ts +15 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/types.ts +98 -27
- package/src/core/tools/web-scrapers/utils.ts +162 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/vimeo.ts +3 -3
- package/src/core/tools/web-scrapers/vscode-marketplace.ts +195 -0
- package/src/core/tools/web-scrapers/w3c.ts +163 -0
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikidata.ts +13 -5
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.ts +7 -3
- package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.ts +72 -20
- package/src/core/tools/write.ts +21 -18
- package/src/core/voice.ts +3 -2
- package/src/lib/worktree/collapse.ts +2 -1
- package/src/lib/worktree/git.ts +2 -18
- package/src/main.ts +59 -3
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +33 -19
- package/src/modes/interactive/components/extensions/extension-list.ts +15 -8
- package/src/modes/interactive/components/hook-editor.ts +2 -1
- package/src/modes/interactive/components/model-selector.ts +19 -4
- package/src/modes/interactive/interactive-mode.ts +41 -63
- package/src/modes/interactive/theme/theme.ts +58 -58
- package/src/modes/rpc/rpc-mode.ts +10 -9
- package/src/prompts/review-request.md +27 -0
- package/src/prompts/reviewer.md +64 -68
- package/src/prompts/tools/output.md +22 -3
- package/src/prompts/tools/task.md +32 -33
- package/src/utils/clipboard.ts +2 -1
- package/examples/extensions/subagent/agents/reviewer.md +0 -35
- package/src/core/tools/web-fetch-handlers/index.ts +0 -69
- package/src/core/tools/web-fetch-handlers/utils.ts +0 -91
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/academic.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/business.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/dev-platforms.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/documentation.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/finance-media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/git-hosting.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/media.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers-2.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-managers.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/package-registries.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/research.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/security.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social-extended.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/social.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/stackexchange.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/standards.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/wikipedia.test.ts +0 -0
- /package/src/core/tools/{web-fetch-handlers → web-scrapers}/youtube.test.ts +0 -0
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closeSync,
|
|
3
|
+
existsSync,
|
|
4
|
+
fsyncSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
openSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readSync,
|
|
9
|
+
statSync,
|
|
10
|
+
writeFileSync,
|
|
11
|
+
writeSync,
|
|
12
|
+
} from "node:fs";
|
|
13
|
+
import { rename as renameAsync } from "node:fs/promises";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
|
|
16
|
+
export interface SessionStorageStat {
|
|
17
|
+
size: number;
|
|
18
|
+
mtimeMs: number;
|
|
19
|
+
mtime: Date;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SessionStorageWriter {
|
|
23
|
+
writeLine(line: string): Promise<void>;
|
|
24
|
+
flush(): Promise<void>;
|
|
25
|
+
fsync(): Promise<void>;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
getError(): Error | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SessionStorage {
|
|
31
|
+
ensureDirSync(dir: string): void;
|
|
32
|
+
existsSync(path: string): boolean;
|
|
33
|
+
readTextSync(path: string): string;
|
|
34
|
+
readTextPrefixSync(path: string, buf: Buffer): number;
|
|
35
|
+
writeTextSync(path: string, content: string): void;
|
|
36
|
+
statSync(path: string): SessionStorageStat;
|
|
37
|
+
listFilesSync(dir: string, pattern: string): string[];
|
|
38
|
+
|
|
39
|
+
exists(path: string): Promise<boolean>;
|
|
40
|
+
readText(path: string): Promise<string>;
|
|
41
|
+
writeText(path: string, content: string): Promise<void>;
|
|
42
|
+
rename(path: string, nextPath: string): Promise<void>;
|
|
43
|
+
unlink(path: string): Promise<void>;
|
|
44
|
+
fsyncDirSync(dir: string): void;
|
|
45
|
+
openWriter(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }): SessionStorageWriter;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toError(value: unknown): Error {
|
|
49
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// FinalizationRegistry to clean up leaked file descriptors
|
|
53
|
+
const writerRegistry = new FinalizationRegistry<number>((fd) => {
|
|
54
|
+
try {
|
|
55
|
+
closeSync(fd);
|
|
56
|
+
} catch {
|
|
57
|
+
// Ignore - fd may already be closed or invalid
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
class FileSessionStorageWriter implements SessionStorageWriter {
|
|
62
|
+
private fd: number;
|
|
63
|
+
private closed = false;
|
|
64
|
+
private error: Error | undefined;
|
|
65
|
+
private onError: ((err: Error) => void) | undefined;
|
|
66
|
+
|
|
67
|
+
constructor(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
|
|
68
|
+
this.onError = options?.onError;
|
|
69
|
+
const flags = options?.flags ?? "a";
|
|
70
|
+
// Ensure parent directory exists
|
|
71
|
+
const dir = dirname(path);
|
|
72
|
+
if (!existsSync(dir)) {
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
}
|
|
75
|
+
// Open file once, keep fd for lifetime
|
|
76
|
+
this.fd = openSync(path, flags === "w" ? "w" : "a");
|
|
77
|
+
// Register for cleanup if abandoned without close()
|
|
78
|
+
writerRegistry.register(this, this.fd, this);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private recordError(err: unknown): Error {
|
|
82
|
+
const error = toError(err);
|
|
83
|
+
if (!this.error) this.error = error;
|
|
84
|
+
this.onError?.(error);
|
|
85
|
+
return error;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async writeLine(line: string): Promise<void> {
|
|
89
|
+
if (this.closed) throw new Error("Writer closed");
|
|
90
|
+
if (this.error) throw this.error;
|
|
91
|
+
try {
|
|
92
|
+
const buf = Buffer.from(line, "utf-8");
|
|
93
|
+
let offset = 0;
|
|
94
|
+
while (offset < buf.length) {
|
|
95
|
+
const written = writeSync(this.fd, buf, offset, buf.length - offset);
|
|
96
|
+
if (written === 0) {
|
|
97
|
+
throw new Error("Short write");
|
|
98
|
+
}
|
|
99
|
+
offset += written;
|
|
100
|
+
}
|
|
101
|
+
} catch (err) {
|
|
102
|
+
throw this.recordError(err);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async flush(): Promise<void> {
|
|
107
|
+
if (this.error) throw this.error;
|
|
108
|
+
// OS buffers are flushed on fsync, nothing to do here
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async fsync(): Promise<void> {
|
|
112
|
+
if (this.closed) throw new Error("Writer closed");
|
|
113
|
+
if (this.error) throw this.error;
|
|
114
|
+
try {
|
|
115
|
+
fsyncSync(this.fd);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
throw this.recordError(err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async close(): Promise<void> {
|
|
122
|
+
if (this.closed) return;
|
|
123
|
+
this.closed = true;
|
|
124
|
+
// Unregister from finalization - we're closing properly
|
|
125
|
+
writerRegistry.unregister(this);
|
|
126
|
+
try {
|
|
127
|
+
closeSync(this.fd);
|
|
128
|
+
} catch {
|
|
129
|
+
// Ignore close errors
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getError(): Error | undefined {
|
|
134
|
+
return this.error;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export class FileSessionStorage implements SessionStorage {
|
|
139
|
+
ensureDirSync(dir: string): void {
|
|
140
|
+
if (!existsSync(dir)) {
|
|
141
|
+
mkdirSync(dir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
existsSync(path: string): boolean {
|
|
146
|
+
return existsSync(path);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
readTextSync(path: string): string {
|
|
150
|
+
return readFileSync(path, "utf-8");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
readTextPrefixSync(path: string, buf: Buffer): number {
|
|
154
|
+
const fd = openSync(path, "r");
|
|
155
|
+
try {
|
|
156
|
+
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
|
|
157
|
+
return bytesRead;
|
|
158
|
+
} finally {
|
|
159
|
+
closeSync(fd);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
writeTextSync(path: string, content: string): void {
|
|
164
|
+
this.ensureDirSync(dirname(path));
|
|
165
|
+
writeFileSync(path, content);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
statSync(path: string): SessionStorageStat {
|
|
169
|
+
const stats = statSync(path);
|
|
170
|
+
return { size: stats.size, mtimeMs: stats.mtimeMs, mtime: stats.mtime };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
listFilesSync(dir: string, pattern: string): string[] {
|
|
174
|
+
try {
|
|
175
|
+
return Array.from(new Bun.Glob(pattern).scanSync(dir)).map((name) => join(dir, name));
|
|
176
|
+
} catch {
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
exists(path: string): Promise<boolean> {
|
|
182
|
+
return Bun.file(path).exists();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
readText(path: string): Promise<string> {
|
|
186
|
+
return Bun.file(path).text();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async writeText(path: string, content: string): Promise<void> {
|
|
190
|
+
await Bun.write(path, content, { createPath: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async rename(path: string, nextPath: string): Promise<void> {
|
|
194
|
+
try {
|
|
195
|
+
await renameAsync(path, nextPath);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
throw toError(err);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
unlink(path: string): Promise<void> {
|
|
202
|
+
return Bun.file(path).unlink();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fsyncDirSync(dir: string): void {
|
|
206
|
+
try {
|
|
207
|
+
const fd = openSync(dir, "r");
|
|
208
|
+
try {
|
|
209
|
+
fsyncSync(fd);
|
|
210
|
+
} finally {
|
|
211
|
+
closeSync(fd);
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
// Best-effort: some platforms/filesystems don't support fsync on directories.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
openWriter(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }): SessionStorageWriter {
|
|
219
|
+
return new FileSessionStorageWriter(path, options);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function matchesPattern(name: string, pattern: string): boolean {
|
|
224
|
+
if (pattern === "*") return true;
|
|
225
|
+
if (pattern.startsWith("*.")) {
|
|
226
|
+
return name.endsWith(pattern.slice(1));
|
|
227
|
+
}
|
|
228
|
+
return name === pattern;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
232
|
+
private storage: MemorySessionStorage;
|
|
233
|
+
private path: string;
|
|
234
|
+
private closed = false;
|
|
235
|
+
private error: Error | undefined;
|
|
236
|
+
private onError: ((err: Error) => void) | undefined;
|
|
237
|
+
private ready: Promise<void>;
|
|
238
|
+
|
|
239
|
+
constructor(
|
|
240
|
+
storage: MemorySessionStorage,
|
|
241
|
+
path: string,
|
|
242
|
+
options?: { flags?: "a" | "w"; onError?: (err: Error) => void },
|
|
243
|
+
) {
|
|
244
|
+
this.storage = storage;
|
|
245
|
+
this.path = path;
|
|
246
|
+
this.onError = options?.onError;
|
|
247
|
+
this.ready = this.initialize(options?.flags ?? "a");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private async initialize(flags: "a" | "w"): Promise<void> {
|
|
251
|
+
if (flags === "w") {
|
|
252
|
+
await this.storage.writeText(this.path, "");
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private recordError(err: unknown): Error {
|
|
257
|
+
const error = toError(err);
|
|
258
|
+
if (!this.error) this.error = error;
|
|
259
|
+
this.onError?.(error);
|
|
260
|
+
return error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async writeLine(line: string): Promise<void> {
|
|
264
|
+
if (this.closed) throw new Error("Writer closed");
|
|
265
|
+
await this.ready;
|
|
266
|
+
if (this.error) throw this.error;
|
|
267
|
+
try {
|
|
268
|
+
const existing = this.storage.existsSync(this.path) ? this.storage.readTextSync(this.path) : "";
|
|
269
|
+
await this.storage.writeText(this.path, `${existing}${line}`);
|
|
270
|
+
} catch (err) {
|
|
271
|
+
throw this.recordError(err);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async flush(): Promise<void> {
|
|
276
|
+
await this.ready;
|
|
277
|
+
if (this.error) throw this.error;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async fsync(): Promise<void> {
|
|
281
|
+
// No-op for in-memory storage
|
|
282
|
+
await this.ready;
|
|
283
|
+
if (this.error) throw this.error;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async close(): Promise<void> {
|
|
287
|
+
if (this.closed) return;
|
|
288
|
+
await this.ready;
|
|
289
|
+
this.closed = true;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
getError(): Error | undefined {
|
|
293
|
+
return this.error;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export class MemorySessionStorage implements SessionStorage {
|
|
298
|
+
private files = new Map<string, { content: string; mtimeMs: number }>();
|
|
299
|
+
|
|
300
|
+
ensureDirSync(_dir: string): void {
|
|
301
|
+
// No-op for in-memory storage.
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
existsSync(path: string): boolean {
|
|
305
|
+
return this.files.has(path);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
readTextSync(path: string): string {
|
|
309
|
+
const entry = this.files.get(path);
|
|
310
|
+
if (!entry) throw new Error(`File not found: ${path}`);
|
|
311
|
+
return entry.content;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
readTextPrefixSync(path: string, buf: Buffer): number {
|
|
315
|
+
const content = this.readTextSync(path);
|
|
316
|
+
return buf.write(content, 0, buf.length, "utf-8");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
writeTextSync(path: string, content: string): void {
|
|
320
|
+
this.files.set(path, { content, mtimeMs: Date.now() });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
statSync(path: string): SessionStorageStat {
|
|
324
|
+
const entry = this.files.get(path);
|
|
325
|
+
if (!entry) throw new Error(`File not found: ${path}`);
|
|
326
|
+
return {
|
|
327
|
+
size: entry.content.length,
|
|
328
|
+
mtimeMs: entry.mtimeMs,
|
|
329
|
+
mtime: new Date(entry.mtimeMs),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
listFilesSync(dir: string, pattern: string): string[] {
|
|
334
|
+
const prefix = dir.endsWith("/") ? dir : `${dir}/`;
|
|
335
|
+
const files: string[] = [];
|
|
336
|
+
for (const path of this.files.keys()) {
|
|
337
|
+
if (!path.startsWith(prefix)) continue;
|
|
338
|
+
const name = path.slice(prefix.length);
|
|
339
|
+
if (name.includes("/") || name.includes("\\")) continue;
|
|
340
|
+
if (!matchesPattern(name, pattern)) continue;
|
|
341
|
+
files.push(path);
|
|
342
|
+
}
|
|
343
|
+
return files;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
exists(path: string): Promise<boolean> {
|
|
347
|
+
return Promise.resolve(this.existsSync(path));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
readText(path: string): Promise<string> {
|
|
351
|
+
return Promise.resolve(this.readTextSync(path));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
writeText(path: string, content: string): Promise<void> {
|
|
355
|
+
this.writeTextSync(path, content);
|
|
356
|
+
return Promise.resolve();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
rename(path: string, nextPath: string): Promise<void> {
|
|
360
|
+
const entry = this.files.get(path);
|
|
361
|
+
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
|
|
362
|
+
this.files.set(nextPath, entry);
|
|
363
|
+
this.files.delete(path);
|
|
364
|
+
return Promise.resolve();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
unlink(path: string): Promise<void> {
|
|
368
|
+
this.files.delete(path);
|
|
369
|
+
return Promise.resolve();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
fsyncDirSync(_dir: string): void {
|
|
373
|
+
// No-op for in-memory storage.
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
openWriter(path: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }): SessionStorageWriter {
|
|
377
|
+
return new MemorySessionStorageWriter(this, path, options);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -70,8 +70,17 @@ export interface ProviderSettings {
|
|
|
70
70
|
image?: ImageProviderOption; // default: "auto" (openrouter > gemini)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
export interface BashInterceptorRule {
|
|
74
|
+
pattern: string;
|
|
75
|
+
flags?: string;
|
|
76
|
+
tool: string;
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
export interface BashInterceptorSettings {
|
|
74
81
|
enabled?: boolean; // default: false (blocks shell commands that have dedicated tools)
|
|
82
|
+
simpleLs?: boolean; // default: true (intercept bare ls commands)
|
|
83
|
+
patterns?: BashInterceptorRule[]; // default: built-in rules
|
|
75
84
|
}
|
|
76
85
|
|
|
77
86
|
export interface GitSettings {
|
|
@@ -191,6 +200,140 @@ export interface Settings {
|
|
|
191
200
|
statusLine?: StatusLineSettings; // Status line configuration
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
export const DEFAULT_BASH_INTERCEPTOR_RULES: BashInterceptorRule[] = [
|
|
204
|
+
{
|
|
205
|
+
pattern: "^\\s*(cat|head|tail|less|more)\\s+",
|
|
206
|
+
tool: "read",
|
|
207
|
+
message: "Use the `read` tool instead of cat/head/tail. It provides better context and handles binary files.",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
pattern: "^\\s*(grep|rg|ripgrep|ag|ack)\\s+",
|
|
211
|
+
tool: "grep",
|
|
212
|
+
message: "Use the `grep` tool instead of grep/rg. It respects .gitignore and provides structured output.",
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
pattern: "^\\s*git(\\s+|$)",
|
|
216
|
+
tool: "git",
|
|
217
|
+
message:
|
|
218
|
+
"Use the `git` tool instead of running git in bash. It provides structured output and safety confirmations.",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
pattern: "^\\s*(find|fd|locate)\\s+.*(-name|-iname|-type|--type|-glob)",
|
|
222
|
+
tool: "find",
|
|
223
|
+
message: "Use the `find` tool instead of find/fd. It respects .gitignore and is faster for glob patterns.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
pattern: "^\\s*sed\\s+(-i|--in-place)",
|
|
227
|
+
tool: "edit",
|
|
228
|
+
message: "Use the `edit` tool instead of sed -i. It provides diff preview and fuzzy matching.",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
pattern: "^\\s*perl\\s+.*-[pn]?i",
|
|
232
|
+
tool: "edit",
|
|
233
|
+
message: "Use the `edit` tool instead of perl -i. It provides diff preview and fuzzy matching.",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
pattern: "^\\s*awk\\s+.*-i\\s+inplace",
|
|
237
|
+
tool: "edit",
|
|
238
|
+
message: "Use the `edit` tool instead of awk -i inplace. It provides diff preview and fuzzy matching.",
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
pattern: "^\\s*(echo|printf|cat\\s*<<)\\s+.*[^|]>\\s*\\S",
|
|
242
|
+
tool: "write",
|
|
243
|
+
message: "Use the `write` tool instead of echo/cat redirection. It handles encoding and provides confirmation.",
|
|
244
|
+
},
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
const DEFAULT_BASH_INTERCEPTOR_SETTINGS: Required<BashInterceptorSettings> = {
|
|
248
|
+
enabled: false,
|
|
249
|
+
simpleLs: true,
|
|
250
|
+
patterns: DEFAULT_BASH_INTERCEPTOR_RULES,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const DEFAULT_SETTINGS: Settings = {
|
|
254
|
+
compaction: { enabled: true, reserveTokens: 16384, keepRecentTokens: 20000 },
|
|
255
|
+
branchSummary: { enabled: false, reserveTokens: 16384 },
|
|
256
|
+
retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
|
|
257
|
+
skills: {
|
|
258
|
+
enabled: true,
|
|
259
|
+
enableCodexUser: true,
|
|
260
|
+
enableClaudeUser: true,
|
|
261
|
+
enableClaudeProject: true,
|
|
262
|
+
enablePiUser: true,
|
|
263
|
+
enablePiProject: true,
|
|
264
|
+
customDirectories: [],
|
|
265
|
+
ignoredSkills: [],
|
|
266
|
+
includeSkills: [],
|
|
267
|
+
},
|
|
268
|
+
commands: { enableClaudeUser: true, enableClaudeProject: true },
|
|
269
|
+
terminal: { showImages: true },
|
|
270
|
+
images: { autoResize: true },
|
|
271
|
+
notifications: { onComplete: "auto" },
|
|
272
|
+
exa: {
|
|
273
|
+
enabled: true,
|
|
274
|
+
enableSearch: true,
|
|
275
|
+
enableLinkedin: false,
|
|
276
|
+
enableCompany: false,
|
|
277
|
+
enableResearcher: false,
|
|
278
|
+
enableWebsets: false,
|
|
279
|
+
},
|
|
280
|
+
bashInterceptor: DEFAULT_BASH_INTERCEPTOR_SETTINGS,
|
|
281
|
+
git: { enabled: false },
|
|
282
|
+
mcp: { enableProjectConfig: true },
|
|
283
|
+
lsp: { formatOnWrite: false, diagnosticsOnWrite: true, diagnosticsOnEdit: false },
|
|
284
|
+
edit: { fuzzyMatch: true },
|
|
285
|
+
ttsr: { enabled: true, contextMode: "discard", repeatMode: "once", repeatGap: 10 },
|
|
286
|
+
voice: {
|
|
287
|
+
enabled: false,
|
|
288
|
+
transcriptionModel: "whisper-1",
|
|
289
|
+
ttsModel: "gpt-4o-mini-tts",
|
|
290
|
+
ttsVoice: "alloy",
|
|
291
|
+
ttsFormat: "wav",
|
|
292
|
+
},
|
|
293
|
+
providers: { webSearch: "auto", image: "auto" },
|
|
294
|
+
} satisfies Settings;
|
|
295
|
+
|
|
296
|
+
function normalizeBashInterceptorRule(rule: unknown): BashInterceptorRule | null {
|
|
297
|
+
if (!rule || typeof rule !== "object" || Array.isArray(rule)) return null;
|
|
298
|
+
|
|
299
|
+
const candidate = rule as Record<string, unknown>;
|
|
300
|
+
const pattern = typeof candidate.pattern === "string" ? candidate.pattern : "";
|
|
301
|
+
const tool = typeof candidate.tool === "string" ? candidate.tool : "";
|
|
302
|
+
const message = typeof candidate.message === "string" ? candidate.message : "";
|
|
303
|
+
const flags = typeof candidate.flags === "string" && candidate.flags.length > 0 ? candidate.flags : undefined;
|
|
304
|
+
|
|
305
|
+
if (!pattern || !tool || !message) return null;
|
|
306
|
+
return { pattern, flags, tool, message };
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function normalizeBashInterceptorSettings(
|
|
310
|
+
settings: BashInterceptorSettings | undefined,
|
|
311
|
+
): Required<BashInterceptorSettings> {
|
|
312
|
+
const enabled = settings?.enabled ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.enabled;
|
|
313
|
+
const simpleLs = settings?.simpleLs ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.simpleLs;
|
|
314
|
+
const rawPatterns = settings?.patterns;
|
|
315
|
+
let patterns: BashInterceptorRule[];
|
|
316
|
+
if (rawPatterns === undefined) {
|
|
317
|
+
patterns = DEFAULT_BASH_INTERCEPTOR_RULES;
|
|
318
|
+
} else if (Array.isArray(rawPatterns)) {
|
|
319
|
+
patterns = rawPatterns
|
|
320
|
+
.map((rule) => normalizeBashInterceptorRule(rule))
|
|
321
|
+
.filter((rule): rule is BashInterceptorRule => rule !== null);
|
|
322
|
+
} else {
|
|
323
|
+
patterns = DEFAULT_BASH_INTERCEPTOR_RULES;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return { enabled, simpleLs, patterns };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function normalizeSettings(settings: Settings): Settings {
|
|
330
|
+
const merged = deepMergeSettings(DEFAULT_SETTINGS, settings);
|
|
331
|
+
return {
|
|
332
|
+
...merged,
|
|
333
|
+
bashInterceptor: normalizeBashInterceptorSettings(merged.bashInterceptor),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
194
337
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
195
338
|
function deepMergeSettings(base: Settings, overrides: Settings): Settings {
|
|
196
339
|
const result: Settings = { ...base };
|
|
@@ -235,7 +378,7 @@ export class SettingsManager {
|
|
|
235
378
|
this.persist = persist;
|
|
236
379
|
this.globalSettings = initialSettings;
|
|
237
380
|
const projectSettings = this.loadProjectSettings();
|
|
238
|
-
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
|
|
381
|
+
this.settings = normalizeSettings(deepMergeSettings(this.globalSettings, projectSettings));
|
|
239
382
|
}
|
|
240
383
|
|
|
241
384
|
/** Create a SettingsManager that loads from files */
|
|
@@ -308,7 +451,7 @@ export class SettingsManager {
|
|
|
308
451
|
|
|
309
452
|
/** Apply additional overrides on top of current settings */
|
|
310
453
|
applyOverrides(overrides: Partial<Settings>): void {
|
|
311
|
-
this.settings = deepMergeSettings(this.settings, overrides);
|
|
454
|
+
this.settings = normalizeSettings(deepMergeSettings(this.settings, overrides));
|
|
312
455
|
}
|
|
313
456
|
|
|
314
457
|
private save(): void {
|
|
@@ -325,7 +468,7 @@ export class SettingsManager {
|
|
|
325
468
|
|
|
326
469
|
// Re-merge project settings into active settings
|
|
327
470
|
const projectSettings = this.loadProjectSettings();
|
|
328
|
-
this.settings = deepMergeSettings(this.globalSettings, projectSettings);
|
|
471
|
+
this.settings = normalizeSettings(deepMergeSettings(this.globalSettings, projectSettings));
|
|
329
472
|
} catch (error) {
|
|
330
473
|
console.error(`Warning: Could not save settings file: ${error}`);
|
|
331
474
|
}
|
|
@@ -680,7 +823,15 @@ export class SettingsManager {
|
|
|
680
823
|
}
|
|
681
824
|
|
|
682
825
|
getBashInterceptorEnabled(): boolean {
|
|
683
|
-
return this.settings.bashInterceptor?.enabled ??
|
|
826
|
+
return this.settings.bashInterceptor?.enabled ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.enabled;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
getBashInterceptorSimpleLsEnabled(): boolean {
|
|
830
|
+
return this.settings.bashInterceptor?.simpleLs ?? DEFAULT_BASH_INTERCEPTOR_SETTINGS.simpleLs;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
getBashInterceptorRules(): BashInterceptorRule[] {
|
|
834
|
+
return [...(this.settings.bashInterceptor?.patterns ?? DEFAULT_BASH_INTERCEPTOR_RULES)];
|
|
684
835
|
}
|
|
685
836
|
|
|
686
837
|
setBashInterceptorEnabled(enabled: boolean): void {
|
|
@@ -2,6 +2,7 @@ import { slashCommandCapability } from "../capability/slash-command";
|
|
|
2
2
|
import type { SlashCommand } from "../discovery";
|
|
3
3
|
import { loadSync } from "../discovery";
|
|
4
4
|
import { parseFrontmatter } from "../discovery/helpers";
|
|
5
|
+
import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Represents a custom slash command loaded from a file
|
|
@@ -15,6 +16,25 @@ export interface FileSlashCommand {
|
|
|
15
16
|
_source?: { providerName: string; level: "user" | "project" | "native" };
|
|
16
17
|
}
|
|
17
18
|
|
|
19
|
+
const EMBEDDED_SLASH_COMMANDS = EMBEDDED_COMMAND_TEMPLATES;
|
|
20
|
+
|
|
21
|
+
function parseCommandTemplate(content: string): { description: string; body: string } {
|
|
22
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
23
|
+
const frontmatterDesc = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
24
|
+
|
|
25
|
+
// Get description from frontmatter or first non-empty line
|
|
26
|
+
let description = frontmatterDesc;
|
|
27
|
+
if (!description) {
|
|
28
|
+
const firstLine = body.split("\n").find((line) => line.trim());
|
|
29
|
+
if (firstLine) {
|
|
30
|
+
description = firstLine.slice(0, 60);
|
|
31
|
+
if (firstLine.length > 60) description += "...";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { description, body };
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
/**
|
|
19
39
|
* Parse command arguments respecting quoted strings (bash-style)
|
|
20
40
|
* Returns array of arguments
|
|
@@ -90,19 +110,8 @@ export interface LoadSlashCommandsOptions {
|
|
|
90
110
|
export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileSlashCommand[] {
|
|
91
111
|
const result = loadSync<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
const {
|
|
95
|
-
const frontmatterDesc = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
96
|
-
|
|
97
|
-
// Get description from frontmatter or first non-empty line
|
|
98
|
-
let description = frontmatterDesc;
|
|
99
|
-
if (!description) {
|
|
100
|
-
const firstLine = body.split("\n").find((line) => line.trim());
|
|
101
|
-
if (firstLine) {
|
|
102
|
-
description = firstLine.slice(0, 60);
|
|
103
|
-
if (firstLine.length > 60) description += "...";
|
|
104
|
-
}
|
|
105
|
-
}
|
|
113
|
+
const fileCommands: FileSlashCommand[] = result.items.map((cmd) => {
|
|
114
|
+
const { description, body } = parseCommandTemplate(cmd.content);
|
|
106
115
|
|
|
107
116
|
// Format source label: "via ProviderName Level"
|
|
108
117
|
const capitalizedLevel = cmd.level.charAt(0).toUpperCase() + cmd.level.slice(1);
|
|
@@ -116,6 +125,23 @@ export function loadSlashCommands(options: LoadSlashCommandsOptions = {}): FileS
|
|
|
116
125
|
_source: { providerName: cmd._source.providerName, level: cmd.level },
|
|
117
126
|
};
|
|
118
127
|
});
|
|
128
|
+
|
|
129
|
+
const seenNames = new Set(fileCommands.map((cmd) => cmd.name));
|
|
130
|
+
for (const cmd of EMBEDDED_SLASH_COMMANDS) {
|
|
131
|
+
const name = cmd.name.replace(/\.md$/, "");
|
|
132
|
+
if (seenNames.has(name)) continue;
|
|
133
|
+
|
|
134
|
+
const { description, body } = parseCommandTemplate(cmd.content);
|
|
135
|
+
fileCommands.push({
|
|
136
|
+
name,
|
|
137
|
+
description,
|
|
138
|
+
content: body,
|
|
139
|
+
source: "bundled",
|
|
140
|
+
});
|
|
141
|
+
seenNames.add(name);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return fileCommands;
|
|
119
145
|
}
|
|
120
146
|
|
|
121
147
|
/**
|