@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.
- package/CHANGELOG.md +61 -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-editor.ts +1 -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/separators.ts +4 -4
- package/src/modes/interactive/components/status-line.ts +45 -35
- package/src/modes/interactive/components/tool-execution.ts +95 -23
- package/src/modes/interactive/interactive-mode.ts +644 -113
- package/src/modes/interactive/theme/defaults/alabaster.json +99 -0
- package/src/modes/interactive/theme/defaults/amethyst.json +103 -0
- package/src/modes/interactive/theme/defaults/anthracite.json +100 -0
- package/src/modes/interactive/theme/defaults/basalt.json +90 -0
- package/src/modes/interactive/theme/defaults/birch.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-abyss.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-aurora.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cavern.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-copper.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-cosmos.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-eclipse.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-ember.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-equinox.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-lunar.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-midnight.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-nebula.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-rainforest.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-reef.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-sakura.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-slate.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-solstice.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-starfall.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-swamp.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-taiga.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-terminal.json +94 -0
- package/src/modes/interactive/theme/defaults/dark-tundra.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-twilight.json +97 -0
- package/src/modes/interactive/theme/defaults/dark-volcanic.json +97 -0
- package/src/modes/interactive/theme/defaults/graphite.json +99 -0
- package/src/modes/interactive/theme/defaults/index.ts +128 -0
- package/src/modes/interactive/theme/defaults/light-aurora-day.json +97 -0
- package/src/modes/interactive/theme/defaults/light-canyon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-cirrus.json +96 -0
- package/src/modes/interactive/theme/defaults/light-coral.json +94 -0
- package/src/modes/interactive/theme/defaults/light-dawn.json +96 -0
- package/src/modes/interactive/theme/defaults/light-dunes.json +97 -0
- package/src/modes/interactive/theme/defaults/light-eucalyptus.json +94 -0
- package/src/modes/interactive/theme/defaults/light-frost.json +94 -0
- package/src/modes/interactive/theme/defaults/light-glacier.json +97 -0
- package/src/modes/interactive/theme/defaults/light-haze.json +96 -0
- package/src/modes/interactive/theme/defaults/light-honeycomb.json +94 -0
- package/src/modes/interactive/theme/defaults/light-lagoon.json +97 -0
- package/src/modes/interactive/theme/defaults/light-lavender.json +94 -0
- package/src/modes/interactive/theme/defaults/light-meadow.json +97 -0
- package/src/modes/interactive/theme/defaults/light-mint.json +94 -0
- package/src/modes/interactive/theme/defaults/light-opal.json +97 -0
- package/src/modes/interactive/theme/defaults/light-orchard.json +97 -0
- package/src/modes/interactive/theme/defaults/light-paper.json +94 -0
- package/src/modes/interactive/theme/defaults/light-prism.json +96 -0
- package/src/modes/interactive/theme/defaults/light-sand.json +94 -0
- package/src/modes/interactive/theme/defaults/light-savanna.json +97 -0
- package/src/modes/interactive/theme/defaults/light-soleil.json +96 -0
- package/src/modes/interactive/theme/defaults/light-wetland.json +97 -0
- package/src/modes/interactive/theme/defaults/light-zenith.json +95 -0
- package/src/modes/interactive/theme/defaults/limestone.json +100 -0
- package/src/modes/interactive/theme/defaults/mahogany.json +104 -0
- package/src/modes/interactive/theme/defaults/marble.json +99 -0
- package/src/modes/interactive/theme/defaults/obsidian.json +90 -0
- package/src/modes/interactive/theme/defaults/onyx.json +90 -0
- package/src/modes/interactive/theme/defaults/pearl.json +99 -0
- package/src/modes/interactive/theme/defaults/porcelain.json +90 -0
- package/src/modes/interactive/theme/defaults/quartz.json +102 -0
- package/src/modes/interactive/theme/defaults/sandstone.json +101 -0
- package/src/modes/interactive/theme/defaults/titanium.json +89 -0
- 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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
|
|
6
|
+
import { logger } from "./logger";
|
|
7
|
+
import type { ModelRegistry } from "./model-registry";
|
|
8
|
+
import { findSmolModel } from "./model-resolver";
|
|
9
|
+
import type { VoiceSettings } from "./settings-manager";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SAMPLE_RATE = 16000;
|
|
12
|
+
const DEFAULT_CHANNELS = 1;
|
|
13
|
+
const DEFAULT_BITS = 16;
|
|
14
|
+
const SUMMARY_MAX_CHARS = 6000;
|
|
15
|
+
|
|
16
|
+
export interface VoiceRecordingHandle {
|
|
17
|
+
filePath: string;
|
|
18
|
+
stop: () => Promise<void>;
|
|
19
|
+
cancel: () => Promise<void>;
|
|
20
|
+
cleanup: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface VoiceTranscriptionResult {
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface VoiceSynthesisResult {
|
|
28
|
+
audio: Uint8Array;
|
|
29
|
+
format: "wav" | "mp3" | "opus" | "aac" | "flac";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildRecordingCommand(filePath: string, sampleRate: number, channels: number): string[] | null {
|
|
33
|
+
const soxPath = Bun.which("sox") ?? Bun.which("rec");
|
|
34
|
+
if (soxPath) {
|
|
35
|
+
return [soxPath, "-d", "-r", String(sampleRate), "-c", String(channels), "-b", String(DEFAULT_BITS), filePath];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const arecordPath = Bun.which("arecord");
|
|
39
|
+
if (arecordPath) {
|
|
40
|
+
return [arecordPath, "-f", "S16_LE", "-r", String(sampleRate), "-c", String(channels), filePath];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ffmpegPath = Bun.which("ffmpeg");
|
|
44
|
+
if (ffmpegPath) {
|
|
45
|
+
const platform = process.platform;
|
|
46
|
+
if (platform === "darwin") {
|
|
47
|
+
// avfoundation default input device; users can override by installing sox for reliability.
|
|
48
|
+
return [
|
|
49
|
+
ffmpegPath,
|
|
50
|
+
"-f",
|
|
51
|
+
"avfoundation",
|
|
52
|
+
"-i",
|
|
53
|
+
":0",
|
|
54
|
+
"-ac",
|
|
55
|
+
String(channels),
|
|
56
|
+
"-ar",
|
|
57
|
+
String(sampleRate),
|
|
58
|
+
"-y",
|
|
59
|
+
filePath,
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
if (platform === "linux") {
|
|
63
|
+
// alsa default input device (commonly "default").
|
|
64
|
+
return [
|
|
65
|
+
ffmpegPath,
|
|
66
|
+
"-f",
|
|
67
|
+
"alsa",
|
|
68
|
+
"-i",
|
|
69
|
+
"default",
|
|
70
|
+
"-ac",
|
|
71
|
+
String(channels),
|
|
72
|
+
"-ar",
|
|
73
|
+
String(sampleRate),
|
|
74
|
+
"-y",
|
|
75
|
+
filePath,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
if (platform === "win32") {
|
|
79
|
+
// dshow default input device name varies; "audio=default" is a best-effort fallback.
|
|
80
|
+
return [
|
|
81
|
+
ffmpegPath,
|
|
82
|
+
"-f",
|
|
83
|
+
"dshow",
|
|
84
|
+
"-i",
|
|
85
|
+
"audio=default",
|
|
86
|
+
"-ac",
|
|
87
|
+
String(channels),
|
|
88
|
+
"-ar",
|
|
89
|
+
String(sampleRate),
|
|
90
|
+
"-y",
|
|
91
|
+
filePath,
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function startVoiceRecording(_settings: VoiceSettings): Promise<VoiceRecordingHandle> {
|
|
100
|
+
const sampleRate = DEFAULT_SAMPLE_RATE;
|
|
101
|
+
const channels = DEFAULT_CHANNELS;
|
|
102
|
+
const filePath = join(tmpdir(), `omp-voice-${Date.now()}.wav`);
|
|
103
|
+
const command = buildRecordingCommand(filePath, sampleRate, channels);
|
|
104
|
+
if (!command) {
|
|
105
|
+
throw new Error("No audio recorder found (install sox, arecord, or ffmpeg).");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
logger.debug("voice: starting recorder", { command });
|
|
109
|
+
const proc = Bun.spawn(command, {
|
|
110
|
+
stdin: "ignore",
|
|
111
|
+
stdout: "ignore",
|
|
112
|
+
stderr: "pipe",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const stop = async (): Promise<void> => {
|
|
116
|
+
try {
|
|
117
|
+
proc.kill();
|
|
118
|
+
} catch {
|
|
119
|
+
// ignore
|
|
120
|
+
}
|
|
121
|
+
await proc.exited;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const cleanup = (): void => {
|
|
125
|
+
try {
|
|
126
|
+
unlinkSync(filePath);
|
|
127
|
+
} catch {
|
|
128
|
+
// ignore cleanup errors
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const cancel = async (): Promise<void> => {
|
|
133
|
+
await stop();
|
|
134
|
+
cleanup();
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
return { filePath, stop, cancel, cleanup };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function transcribeAudio(
|
|
141
|
+
filePath: string,
|
|
142
|
+
apiKey: string,
|
|
143
|
+
settings: VoiceSettings,
|
|
144
|
+
): Promise<VoiceTranscriptionResult> {
|
|
145
|
+
const file = Bun.file(filePath);
|
|
146
|
+
const buffer = await file.arrayBuffer();
|
|
147
|
+
const blob = new File([buffer], "speech.wav", { type: "audio/wav" });
|
|
148
|
+
const form = new FormData();
|
|
149
|
+
form.append("file", blob);
|
|
150
|
+
form.append("model", settings.transcriptionModel ?? "whisper-1");
|
|
151
|
+
if (settings.transcriptionLanguage) {
|
|
152
|
+
form.append("language", settings.transcriptionLanguage);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
158
|
+
body: form,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const errText = await response.text();
|
|
163
|
+
throw new Error(`Whisper transcription failed: ${response.status} ${errText}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const data = (await response.json()) as { text?: string };
|
|
167
|
+
return { text: (data.text ?? "").trim() };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function synthesizeSpeech(
|
|
171
|
+
text: string,
|
|
172
|
+
apiKey: string,
|
|
173
|
+
settings: VoiceSettings,
|
|
174
|
+
): Promise<VoiceSynthesisResult> {
|
|
175
|
+
const format = settings.ttsFormat ?? "wav";
|
|
176
|
+
const response = await fetch("https://api.openai.com/v1/audio/speech", {
|
|
177
|
+
method: "POST",
|
|
178
|
+
headers: {
|
|
179
|
+
Authorization: `Bearer ${apiKey}`,
|
|
180
|
+
"Content-Type": "application/json",
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({
|
|
183
|
+
model: settings.ttsModel ?? "tts-1",
|
|
184
|
+
voice: settings.ttsVoice ?? "alloy",
|
|
185
|
+
format,
|
|
186
|
+
input: text,
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
const errText = await response.text();
|
|
192
|
+
throw new Error(`TTS synthesis failed: ${response.status} ${errText}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const audio = new Uint8Array(await response.arrayBuffer());
|
|
196
|
+
return { audio, format };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function getPlayerCommand(filePath: string, format: VoiceSynthesisResult["format"]): string[] | null {
|
|
200
|
+
const platform = process.platform;
|
|
201
|
+
if (platform === "darwin") {
|
|
202
|
+
const afplay = Bun.which("afplay");
|
|
203
|
+
if (afplay) return [afplay, filePath];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (platform === "linux") {
|
|
207
|
+
const paplay = Bun.which("paplay");
|
|
208
|
+
if (paplay) return [paplay, filePath];
|
|
209
|
+
const aplay = Bun.which("aplay");
|
|
210
|
+
if (aplay) return [aplay, filePath];
|
|
211
|
+
const ffplay = Bun.which("ffplay");
|
|
212
|
+
if (ffplay) return [ffplay, "-autoexit", "-nodisp", filePath];
|
|
213
|
+
const play = Bun.which("play");
|
|
214
|
+
if (play) return [play, filePath];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (platform === "win32") {
|
|
218
|
+
if (format !== "wav") {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const ps = Bun.which("powershell");
|
|
222
|
+
if (ps) {
|
|
223
|
+
return [
|
|
224
|
+
ps,
|
|
225
|
+
"-NoProfile",
|
|
226
|
+
"-Command",
|
|
227
|
+
`(New-Object Media.SoundPlayer '${filePath.replace(/'/g, "''")}').PlaySync()`,
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function playAudio(audio: Uint8Array, format: VoiceSynthesisResult["format"]): Promise<void> {
|
|
236
|
+
const filePath = join(tmpdir(), `omp-voice-tts-${Date.now()}.${format}`);
|
|
237
|
+
await Bun.write(filePath, audio);
|
|
238
|
+
|
|
239
|
+
const command = getPlayerCommand(filePath, format);
|
|
240
|
+
if (!command) {
|
|
241
|
+
throw new Error("No audio player available for playback.");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const proc = Bun.spawn(command, {
|
|
245
|
+
stdin: "ignore",
|
|
246
|
+
stdout: "ignore",
|
|
247
|
+
stderr: "pipe",
|
|
248
|
+
});
|
|
249
|
+
await proc.exited;
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
unlinkSync(filePath);
|
|
253
|
+
} catch {
|
|
254
|
+
// ignore cleanup errors
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function extractTextFromResponse(response: { content: Array<{ type: string; text?: string }> }): string {
|
|
259
|
+
let text = "";
|
|
260
|
+
for (const content of response.content) {
|
|
261
|
+
if (content.type === "text" && content.text) {
|
|
262
|
+
text += content.text;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return text.trim();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function summarizeForVoice(
|
|
269
|
+
text: string,
|
|
270
|
+
registry: ModelRegistry,
|
|
271
|
+
savedSmolModel?: string,
|
|
272
|
+
): Promise<string | null> {
|
|
273
|
+
const model = await findSmolModel(registry, savedSmolModel);
|
|
274
|
+
if (!model) {
|
|
275
|
+
logger.debug("voice: no smol model found for summary");
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const apiKey = await registry.getApiKey(model);
|
|
280
|
+
if (!apiKey) {
|
|
281
|
+
logger.debug("voice: no API key for summary model", { provider: model.provider, id: model.id });
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const truncated = text.length > SUMMARY_MAX_CHARS ? `${text.slice(0, SUMMARY_MAX_CHARS)}...` : text;
|
|
286
|
+
const request = {
|
|
287
|
+
model: `${model.provider}/${model.id}`,
|
|
288
|
+
systemPrompt: voiceSummaryPrompt,
|
|
289
|
+
userMessage: `<assistant_response>\n${truncated}\n</assistant_response>`,
|
|
290
|
+
};
|
|
291
|
+
logger.debug("voice: summary request", request);
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const response = await completeSimple(
|
|
295
|
+
model as Model<any>,
|
|
296
|
+
{
|
|
297
|
+
systemPrompt: request.systemPrompt,
|
|
298
|
+
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
299
|
+
},
|
|
300
|
+
{ apiKey, maxTokens: 120 },
|
|
301
|
+
);
|
|
302
|
+
const summary = extractTextFromResponse(response);
|
|
303
|
+
return summary || null;
|
|
304
|
+
} catch (error) {
|
|
305
|
+
logger.debug("voice: summary error", { error: error instanceof Error ? error.message : String(error) });
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
package/src/discovery/builtin.ts
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* .pi is an alias for backwards compatibility.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { basename, dirname, join } from "
|
|
8
|
+
import { basename, dirname, isAbsolute, join, resolve } from "path";
|
|
9
9
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
10
10
|
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
|
|
11
|
+
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
11
12
|
import { type Hook, hookCapability } from "../capability/hook";
|
|
12
13
|
import { registerProvider } from "../capability/index";
|
|
13
14
|
import { type Instruction, instructionCapability } from "../capability/instruction";
|
|
@@ -22,7 +23,9 @@ import { type CustomTool, toolCapability } from "../capability/tool";
|
|
|
22
23
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
23
24
|
import {
|
|
24
25
|
createSourceMeta,
|
|
26
|
+
discoverExtensionModulePaths,
|
|
25
27
|
expandEnvVarsDeep,
|
|
28
|
+
getExtensionNameFromPath,
|
|
26
29
|
loadFilesFromDir,
|
|
27
30
|
parseFrontmatter,
|
|
28
31
|
parseJSON,
|
|
@@ -361,6 +364,77 @@ registerProvider<Prompt>(promptCapability.id, {
|
|
|
361
364
|
load: loadPrompts,
|
|
362
365
|
});
|
|
363
366
|
|
|
367
|
+
// Extension Modules
|
|
368
|
+
function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
|
|
369
|
+
const items: ExtensionModule[] = [];
|
|
370
|
+
const warnings: string[] = [];
|
|
371
|
+
|
|
372
|
+
const resolveExtensionPath = (rawPath: string): string => {
|
|
373
|
+
if (rawPath.startsWith("~/")) {
|
|
374
|
+
return join(ctx.home, rawPath.slice(2));
|
|
375
|
+
}
|
|
376
|
+
if (rawPath.startsWith("~")) {
|
|
377
|
+
return join(ctx.home, rawPath.slice(1));
|
|
378
|
+
}
|
|
379
|
+
if (isAbsolute(rawPath)) {
|
|
380
|
+
return rawPath;
|
|
381
|
+
}
|
|
382
|
+
return resolve(ctx.cwd, rawPath);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const addExtensionPath = (extPath: string, level: "user" | "project"): void => {
|
|
386
|
+
items.push({
|
|
387
|
+
name: getExtensionNameFromPath(extPath),
|
|
388
|
+
path: extPath,
|
|
389
|
+
level,
|
|
390
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, level),
|
|
391
|
+
});
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
for (const { dir, level } of getConfigDirs(ctx)) {
|
|
395
|
+
const extensionsDir = join(dir, "extensions");
|
|
396
|
+
const discovered = discoverExtensionModulePaths(ctx, extensionsDir);
|
|
397
|
+
for (const extPath of discovered) {
|
|
398
|
+
addExtensionPath(extPath, level);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const settingsPath = join(dir, "settings.json");
|
|
402
|
+
const settingsContent = ctx.fs.readFile(settingsPath);
|
|
403
|
+
if (settingsContent) {
|
|
404
|
+
const settingsData = parseJSON<{ extensions?: unknown }>(settingsContent);
|
|
405
|
+
const extensions = settingsData?.extensions;
|
|
406
|
+
if (Array.isArray(extensions)) {
|
|
407
|
+
for (const entry of extensions) {
|
|
408
|
+
if (typeof entry !== "string") {
|
|
409
|
+
warnings.push(`Invalid extension path in ${settingsPath}: ${String(entry)}`);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
const resolvedPath = resolveExtensionPath(entry);
|
|
413
|
+
if (ctx.fs.isDir(resolvedPath)) {
|
|
414
|
+
for (const extPath of discoverExtensionModulePaths(ctx, resolvedPath)) {
|
|
415
|
+
addExtensionPath(extPath, level);
|
|
416
|
+
}
|
|
417
|
+
} else if (ctx.fs.isFile(resolvedPath)) {
|
|
418
|
+
addExtensionPath(resolvedPath, level);
|
|
419
|
+
} else {
|
|
420
|
+
warnings.push(`Extension path not found: ${resolvedPath}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { items, warnings };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
registerProvider<ExtensionModule>(extensionModuleCapability.id, {
|
|
431
|
+
id: PROVIDER_ID,
|
|
432
|
+
displayName: DISPLAY_NAME,
|
|
433
|
+
description: DESCRIPTION,
|
|
434
|
+
priority: PRIORITY,
|
|
435
|
+
load: loadExtensionModules,
|
|
436
|
+
});
|
|
437
|
+
|
|
364
438
|
// Extensions
|
|
365
439
|
function loadExtensions(ctx: LoadContext): LoadResult<Extension> {
|
|
366
440
|
const items: Extension[] = [];
|
package/src/discovery/claude.ts
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Priority: 80 (tool-specific, below builtin but above shared standards)
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { dirname, join, sep } from "
|
|
8
|
+
import { dirname, join, sep } from "path";
|
|
9
9
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
10
|
+
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
10
11
|
import { type Hook, hookCapability } from "../capability/hook";
|
|
11
12
|
import { registerProvider } from "../capability/index";
|
|
12
13
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
@@ -19,7 +20,9 @@ import type { LoadContext, LoadResult } from "../capability/types";
|
|
|
19
20
|
import {
|
|
20
21
|
calculateDepth,
|
|
21
22
|
createSourceMeta,
|
|
23
|
+
discoverExtensionModulePaths,
|
|
22
24
|
expandEnvVarsDeep,
|
|
25
|
+
getExtensionNameFromPath,
|
|
23
26
|
loadFilesFromDir,
|
|
24
27
|
parseFrontmatter,
|
|
25
28
|
parseJSON,
|
|
@@ -292,6 +295,41 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
|
|
|
292
295
|
return { items, warnings };
|
|
293
296
|
}
|
|
294
297
|
|
|
298
|
+
// =============================================================================
|
|
299
|
+
// Extension Modules
|
|
300
|
+
// =============================================================================
|
|
301
|
+
|
|
302
|
+
function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
|
|
303
|
+
const items: ExtensionModule[] = [];
|
|
304
|
+
const warnings: string[] = [];
|
|
305
|
+
|
|
306
|
+
const userBase = getUserClaude(ctx);
|
|
307
|
+
const userExtensionsDir = join(userBase, "extensions");
|
|
308
|
+
for (const extPath of discoverExtensionModulePaths(ctx, userExtensionsDir)) {
|
|
309
|
+
items.push({
|
|
310
|
+
name: getExtensionNameFromPath(extPath),
|
|
311
|
+
path: extPath,
|
|
312
|
+
level: "user",
|
|
313
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const projectBase = getProjectClaude(ctx);
|
|
318
|
+
if (projectBase) {
|
|
319
|
+
const projectExtensionsDir = join(projectBase, "extensions");
|
|
320
|
+
for (const extPath of discoverExtensionModulePaths(ctx, projectExtensionsDir)) {
|
|
321
|
+
items.push({
|
|
322
|
+
name: getExtensionNameFromPath(extPath),
|
|
323
|
+
path: extPath,
|
|
324
|
+
level: "project",
|
|
325
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return { items, warnings };
|
|
331
|
+
}
|
|
332
|
+
|
|
295
333
|
// =============================================================================
|
|
296
334
|
// Slash Commands
|
|
297
335
|
// =============================================================================
|
|
@@ -582,6 +620,14 @@ registerProvider<Skill>(skillCapability.id, {
|
|
|
582
620
|
load: loadSkills,
|
|
583
621
|
});
|
|
584
622
|
|
|
623
|
+
registerProvider<ExtensionModule>(extensionModuleCapability.id, {
|
|
624
|
+
id: PROVIDER_ID,
|
|
625
|
+
displayName: DISPLAY_NAME,
|
|
626
|
+
description: "Load extension modules from .claude/extensions",
|
|
627
|
+
priority: PRIORITY,
|
|
628
|
+
load: loadExtensionModules,
|
|
629
|
+
});
|
|
630
|
+
|
|
585
631
|
registerProvider<SlashCommand>(slashCommandCapability.id, {
|
|
586
632
|
id: PROVIDER_ID,
|
|
587
633
|
displayName: DISPLAY_NAME,
|
package/src/discovery/codex.ts
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* User directory: ~/.codex
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { join } from "
|
|
10
|
+
import { join } from "path";
|
|
11
11
|
import { parse as parseToml } from "smol-toml";
|
|
12
12
|
import type { ContextFile } from "../capability/context-file";
|
|
13
13
|
import { contextFileCapability } from "../capability/context-file";
|
|
14
|
+
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
14
15
|
import type { Hook } from "../capability/hook";
|
|
15
16
|
import { hookCapability } from "../capability/hook";
|
|
16
17
|
import { registerProvider } from "../capability/index";
|
|
@@ -27,7 +28,14 @@ import { slashCommandCapability } from "../capability/slash-command";
|
|
|
27
28
|
import type { CustomTool } from "../capability/tool";
|
|
28
29
|
import { toolCapability } from "../capability/tool";
|
|
29
30
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
30
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
createSourceMeta,
|
|
33
|
+
discoverExtensionModulePaths,
|
|
34
|
+
getExtensionNameFromPath,
|
|
35
|
+
loadFilesFromDir,
|
|
36
|
+
parseFrontmatter,
|
|
37
|
+
SOURCE_PATHS,
|
|
38
|
+
} from "./helpers";
|
|
31
39
|
|
|
32
40
|
const PROVIDER_ID = "codex";
|
|
33
41
|
const DISPLAY_NAME = "OpenAI Codex";
|
|
@@ -251,6 +259,42 @@ function loadSkills(ctx: LoadContext): LoadResult<Skill> {
|
|
|
251
259
|
return { items, warnings };
|
|
252
260
|
}
|
|
253
261
|
|
|
262
|
+
// =============================================================================
|
|
263
|
+
// Extension Modules (extensions/)
|
|
264
|
+
// =============================================================================
|
|
265
|
+
|
|
266
|
+
function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
|
|
267
|
+
const items: ExtensionModule[] = [];
|
|
268
|
+
const warnings: string[] = [];
|
|
269
|
+
|
|
270
|
+
// User level: ~/.codex/extensions/
|
|
271
|
+
const userExtensionsDir = join(ctx.home, SOURCE_PATHS.codex.userBase, "extensions");
|
|
272
|
+
for (const extPath of discoverExtensionModulePaths(ctx, userExtensionsDir)) {
|
|
273
|
+
items.push({
|
|
274
|
+
name: getExtensionNameFromPath(extPath),
|
|
275
|
+
path: extPath,
|
|
276
|
+
level: "user",
|
|
277
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Project level: .codex/extensions/
|
|
282
|
+
const codexDir = ctx.fs.walkUp(".codex", { dir: true });
|
|
283
|
+
if (codexDir) {
|
|
284
|
+
const projectExtensionsDir = join(codexDir, "extensions");
|
|
285
|
+
for (const extPath of discoverExtensionModulePaths(ctx, projectExtensionsDir)) {
|
|
286
|
+
items.push({
|
|
287
|
+
name: getExtensionNameFromPath(extPath),
|
|
288
|
+
path: extPath,
|
|
289
|
+
level: "project",
|
|
290
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return { items, warnings };
|
|
296
|
+
}
|
|
297
|
+
|
|
254
298
|
// =============================================================================
|
|
255
299
|
// Slash Commands (commands/)
|
|
256
300
|
// =============================================================================
|
|
@@ -530,6 +574,14 @@ registerProvider<Skill>(skillCapability.id, {
|
|
|
530
574
|
load: loadSkills,
|
|
531
575
|
});
|
|
532
576
|
|
|
577
|
+
registerProvider<ExtensionModule>(extensionModuleCapability.id, {
|
|
578
|
+
id: PROVIDER_ID,
|
|
579
|
+
displayName: DISPLAY_NAME,
|
|
580
|
+
description: "Load extension modules from ~/.codex/extensions and .codex/extensions/",
|
|
581
|
+
priority: PRIORITY,
|
|
582
|
+
load: loadExtensionModules,
|
|
583
|
+
});
|
|
584
|
+
|
|
533
585
|
registerProvider<SlashCommand>(slashCommandCapability.id, {
|
|
534
586
|
id: PROVIDER_ID,
|
|
535
587
|
displayName: DISPLAY_NAME,
|
package/src/discovery/gemini.ts
CHANGED
|
@@ -16,15 +16,25 @@
|
|
|
16
16
|
* - settings: From settings.json
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { dirname, join, sep } from "
|
|
19
|
+
import { dirname, join, sep } from "path";
|
|
20
20
|
import { type ContextFile, contextFileCapability } from "../capability/context-file";
|
|
21
21
|
import { type Extension, type ExtensionManifest, extensionCapability } from "../capability/extension";
|
|
22
|
+
import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module";
|
|
22
23
|
import { registerProvider } from "../capability/index";
|
|
23
24
|
import { type MCPServer, mcpCapability } from "../capability/mcp";
|
|
24
25
|
import { type Settings, settingsCapability } from "../capability/settings";
|
|
25
26
|
import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
|
|
26
27
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
27
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
calculateDepth,
|
|
30
|
+
createSourceMeta,
|
|
31
|
+
discoverExtensionModulePaths,
|
|
32
|
+
expandEnvVarsDeep,
|
|
33
|
+
getExtensionNameFromPath,
|
|
34
|
+
getProjectPath,
|
|
35
|
+
getUserPath,
|
|
36
|
+
parseJSON,
|
|
37
|
+
} from "./helpers";
|
|
28
38
|
|
|
29
39
|
const PROVIDER_ID = "gemini";
|
|
30
40
|
const DISPLAY_NAME = "Gemini CLI";
|
|
@@ -236,6 +246,41 @@ function loadExtensionsFromDir(
|
|
|
236
246
|
return { items, warnings };
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
// =============================================================================
|
|
250
|
+
// Extension Modules
|
|
251
|
+
// =============================================================================
|
|
252
|
+
|
|
253
|
+
function loadExtensionModules(ctx: LoadContext): LoadResult<ExtensionModule> {
|
|
254
|
+
const items: ExtensionModule[] = [];
|
|
255
|
+
const warnings: string[] = [];
|
|
256
|
+
|
|
257
|
+
const userExtensionsDir = getUserPath(ctx, "gemini", "extensions");
|
|
258
|
+
if (userExtensionsDir) {
|
|
259
|
+
for (const extPath of discoverExtensionModulePaths(ctx, userExtensionsDir)) {
|
|
260
|
+
items.push({
|
|
261
|
+
name: getExtensionNameFromPath(extPath),
|
|
262
|
+
path: extPath,
|
|
263
|
+
level: "user",
|
|
264
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "user"),
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const projectExtensionsDir = getProjectPath(ctx, "gemini", "extensions");
|
|
270
|
+
if (projectExtensionsDir) {
|
|
271
|
+
for (const extPath of discoverExtensionModulePaths(ctx, projectExtensionsDir)) {
|
|
272
|
+
items.push({
|
|
273
|
+
name: getExtensionNameFromPath(extPath),
|
|
274
|
+
path: extPath,
|
|
275
|
+
level: "project",
|
|
276
|
+
_source: createSourceMeta(PROVIDER_ID, extPath, "project"),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { items, warnings };
|
|
282
|
+
}
|
|
283
|
+
|
|
239
284
|
// =============================================================================
|
|
240
285
|
// Settings
|
|
241
286
|
// =============================================================================
|
|
@@ -359,6 +404,14 @@ registerProvider(extensionCapability.id, {
|
|
|
359
404
|
load: loadExtensions,
|
|
360
405
|
});
|
|
361
406
|
|
|
407
|
+
registerProvider(extensionModuleCapability.id, {
|
|
408
|
+
id: PROVIDER_ID,
|
|
409
|
+
displayName: DISPLAY_NAME,
|
|
410
|
+
description: "Load extension modules from ~/.gemini/extensions/ and .gemini/extensions/",
|
|
411
|
+
priority: PRIORITY,
|
|
412
|
+
load: loadExtensionModules,
|
|
413
|
+
});
|
|
414
|
+
|
|
362
415
|
registerProvider(settingsCapability.id, {
|
|
363
416
|
id: PROVIDER_ID,
|
|
364
417
|
displayName: DISPLAY_NAME,
|