@oh-my-pi/pi-coding-agent 12.3.0 → 12.5.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 +66 -0
- package/docs/custom-tools.md +21 -6
- package/docs/extensions.md +20 -0
- package/package.json +12 -12
- package/src/cli/setup-cli.ts +62 -2
- package/src/commands/setup.ts +1 -1
- package/src/config/keybindings.ts +6 -2
- package/src/config/settings-schema.ts +58 -4
- package/src/config/settings.ts +23 -9
- package/src/debug/index.ts +26 -19
- package/src/debug/log-formatting.ts +60 -0
- package/src/debug/log-viewer.ts +903 -0
- package/src/debug/report-bundle.ts +87 -8
- package/src/discovery/helpers.ts +131 -137
- package/src/extensibility/custom-tools/types.ts +44 -6
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/hooks/types.ts +60 -0
- package/src/extensibility/skills.ts +4 -2
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +7 -1
- package/src/memories/index.ts +11 -7
- package/src/modes/components/bash-execution.ts +16 -9
- package/src/modes/components/custom-editor.ts +8 -0
- package/src/modes/components/python-execution.ts +16 -7
- package/src/modes/components/settings-selector.ts +29 -14
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/command-controller.ts +3 -1
- package/src/modes/controllers/event-controller.ts +7 -0
- package/src/modes/controllers/input-controller.ts +23 -2
- package/src/modes/controllers/selector-controller.ts +9 -7
- package/src/modes/interactive-mode.ts +84 -1
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +2 -0
- package/src/modes/theme/theme.ts +163 -7
- package/src/modes/types.ts +1 -0
- package/src/patch/hashline.ts +2 -1
- package/src/patch/shared.ts +44 -13
- package/src/prompts/system/plan-mode-approved.md +5 -0
- package/src/prompts/system/subagent-system-prompt.md +1 -0
- package/src/prompts/system/system-prompt.md +10 -0
- package/src/prompts/tools/todo-write.md +3 -1
- package/src/sdk.ts +82 -9
- package/src/session/agent-session.ts +137 -29
- package/src/session/streaming-output.ts +1 -1
- package/src/stt/downloader.ts +71 -0
- package/src/stt/index.ts +3 -0
- package/src/stt/recorder.ts +351 -0
- package/src/stt/setup.ts +52 -0
- package/src/stt/stt-controller.ts +160 -0
- package/src/stt/transcribe.py +70 -0
- package/src/stt/transcriber.ts +91 -0
- package/src/task/executor.ts +10 -2
- package/src/tools/bash-interactive.ts +10 -6
- package/src/tools/fetch.ts +1 -1
- package/src/tools/output-meta.ts +6 -2
- package/src/web/scrapers/types.ts +1 -0
|
@@ -7,15 +7,27 @@ import * as fs from "node:fs/promises";
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import type { WorkProfile } from "@oh-my-pi/pi-natives";
|
|
9
9
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
10
|
-
import { getLogPath, getReportsDir } from "@oh-my-pi/pi-utils/dirs";
|
|
10
|
+
import { APP_NAME, getLogPath, getLogsDir, getReportsDir } from "@oh-my-pi/pi-utils/dirs";
|
|
11
11
|
import type { CpuProfile, HeapSnapshot } from "./profiler";
|
|
12
12
|
import { collectSystemInfo, sanitizeEnv } from "./system-info";
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
14
|
+
/** Maximum number of log lines to load into memory at once. */
|
|
15
|
+
const MAX_LOG_LINES = 5000;
|
|
16
|
+
|
|
17
|
+
/** Maximum bytes to read from the tail of a log file (2 MB). */
|
|
18
|
+
const MAX_LOG_BYTES = 2 * 1024 * 1024;
|
|
19
|
+
/** Read last N lines from a file, reading at most `maxBytes` from the tail. */
|
|
20
|
+
async function readLastLines(filePath: string, n: number, maxBytes = MAX_LOG_BYTES): Promise<string> {
|
|
16
21
|
try {
|
|
17
|
-
const
|
|
22
|
+
const file = Bun.file(filePath);
|
|
23
|
+
const size = file.size;
|
|
24
|
+
const start = Math.max(0, size - maxBytes);
|
|
25
|
+
const content = start > 0 ? await file.slice(start, size).text() : await file.text();
|
|
18
26
|
const lines = content.split("\n");
|
|
27
|
+
// If we sliced mid-file, drop the first (partial) line
|
|
28
|
+
if (start > 0 && lines.length > 0) {
|
|
29
|
+
lines.shift();
|
|
30
|
+
}
|
|
19
31
|
return lines.slice(-n).join("\n");
|
|
20
32
|
} catch (err) {
|
|
21
33
|
if (isEnoent(err)) return "";
|
|
@@ -41,6 +53,12 @@ export interface ReportBundleResult {
|
|
|
41
53
|
files: string[];
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
export interface DebugLogSource {
|
|
57
|
+
getInitialText(): Promise<string>;
|
|
58
|
+
hasOlderLogs(): boolean;
|
|
59
|
+
loadOlderLogs(limitDays?: number): Promise<string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
44
62
|
/**
|
|
45
63
|
* Create a debug report bundle.
|
|
46
64
|
*
|
|
@@ -209,10 +227,71 @@ async function addSubagentSessions(
|
|
|
209
227
|
}
|
|
210
228
|
}
|
|
211
229
|
|
|
212
|
-
/** Get recent log entries for display */
|
|
213
|
-
export async function
|
|
214
|
-
|
|
215
|
-
|
|
230
|
+
/** Get recent log entries for display (tail-limited to avoid OOM on large files). */
|
|
231
|
+
export async function getLogText(): Promise<string> {
|
|
232
|
+
return readLastLines(getLogPath(), MAX_LOG_LINES);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const LOG_FILE_PATTERN = new RegExp(`^${APP_NAME}\\.(\\d{4}-\\d{2}-\\d{2})\\.log$`);
|
|
236
|
+
|
|
237
|
+
export async function createDebugLogSource(): Promise<DebugLogSource> {
|
|
238
|
+
const logsDir = getLogsDir();
|
|
239
|
+
const todayPath = getLogPath();
|
|
240
|
+
const todayName = path.basename(todayPath);
|
|
241
|
+
let olderFiles: string[] = [];
|
|
242
|
+
try {
|
|
243
|
+
const entries = await fs.readdir(logsDir, { withFileTypes: true });
|
|
244
|
+
const datedFiles = entries
|
|
245
|
+
.filter(entry => entry.isFile())
|
|
246
|
+
.map(entry => {
|
|
247
|
+
const match = LOG_FILE_PATTERN.exec(entry.name);
|
|
248
|
+
return match ? { name: entry.name, date: match[1] } : undefined;
|
|
249
|
+
})
|
|
250
|
+
.filter((entry): entry is { name: string; date: string } => entry !== undefined)
|
|
251
|
+
.filter(entry => entry.name !== todayName)
|
|
252
|
+
.sort((a, b) => b.date.localeCompare(a.date));
|
|
253
|
+
olderFiles = datedFiles.map(entry => entry.name);
|
|
254
|
+
} catch {
|
|
255
|
+
olderFiles = [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let cursor = 0;
|
|
259
|
+
|
|
260
|
+
const getInitialText = async (): Promise<string> => {
|
|
261
|
+
return readLastLines(todayPath, MAX_LOG_LINES);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const hasOlderLogs = (): boolean => cursor < olderFiles.length;
|
|
265
|
+
|
|
266
|
+
const loadOlderLogs = async (limitDays: number = 1): Promise<string> => {
|
|
267
|
+
if (!hasOlderLogs()) {
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
const count = Math.max(1, limitDays);
|
|
271
|
+
const slice = olderFiles.slice(cursor, cursor + count);
|
|
272
|
+
cursor += slice.length;
|
|
273
|
+
const chunks: string[] = [];
|
|
274
|
+
for (const filename of slice.reverse()) {
|
|
275
|
+
const filePath = path.join(logsDir, filename);
|
|
276
|
+
try {
|
|
277
|
+
const content = await readLastLines(filePath, MAX_LOG_LINES);
|
|
278
|
+
if (content.length > 0) {
|
|
279
|
+
chunks.push(content);
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
if (!isEnoent(err)) {
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return chunks.filter(chunk => chunk.length > 0).join("\n");
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
getInitialText,
|
|
292
|
+
hasOlderLogs,
|
|
293
|
+
loadOlderLogs,
|
|
294
|
+
};
|
|
216
295
|
}
|
|
217
296
|
|
|
218
297
|
/** Calculate total size of artifact cache */
|
package/src/discovery/helpers.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
import * as os from "node:os";
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
7
|
+
import { FileType, glob } from "@oh-my-pi/pi-natives";
|
|
7
8
|
import { CONFIG_DIR_NAME } from "@oh-my-pi/pi-utils/dirs";
|
|
8
|
-
import {
|
|
9
|
+
import { readFile } from "../capability/fs";
|
|
9
10
|
import type { Skill, SkillFrontmatter } from "../capability/skill";
|
|
10
11
|
import type { LoadContext, LoadResult, SourceMeta } from "../capability/types";
|
|
11
12
|
import { parseFrontmatter } from "../utils/frontmatter";
|
|
12
|
-
import {
|
|
13
|
+
import type { IgnoreMatcher } from "../utils/ignore-files";
|
|
13
14
|
|
|
14
15
|
const VALID_THINKING_LEVELS: readonly string[] = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
15
16
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
@@ -224,6 +225,20 @@ export function parseAgentFields(frontmatter: Record<string, unknown>): ParsedAg
|
|
|
224
225
|
return { name, description, tools, spawns, model, output, thinkingLevel };
|
|
225
226
|
}
|
|
226
227
|
|
|
228
|
+
async function globIf(
|
|
229
|
+
dir: string,
|
|
230
|
+
pattern: string,
|
|
231
|
+
fileType: FileType,
|
|
232
|
+
recursive: boolean = true,
|
|
233
|
+
): Promise<Array<{ path: string }>> {
|
|
234
|
+
try {
|
|
235
|
+
const result = await glob({ pattern, path: dir, gitignore: true, hidden: false, fileType, recursive });
|
|
236
|
+
return result.matches;
|
|
237
|
+
} catch {
|
|
238
|
+
return [];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
227
242
|
export async function loadSkillsFromDir(
|
|
228
243
|
_ctx: LoadContext,
|
|
229
244
|
options: {
|
|
@@ -236,39 +251,42 @@ export async function loadSkillsFromDir(
|
|
|
236
251
|
const items: Skill[] = [];
|
|
237
252
|
const warnings: string[] = [];
|
|
238
253
|
const { dir, level, providerId, requireDescription = false } = options;
|
|
254
|
+
// Use native glob to find all SKILL.md files one level deep
|
|
255
|
+
// Pattern */SKILL.md matches <dir>/<subdir>/SKILL.md
|
|
256
|
+
const discoveredMatches = new Set<string>();
|
|
257
|
+
for (const match of await globIf(dir, "*/SKILL.md", FileType.File)) {
|
|
258
|
+
discoveredMatches.add(match.path);
|
|
259
|
+
}
|
|
260
|
+
for (const match of await globIf(dir, "*", FileType.Dir, false)) {
|
|
261
|
+
const skillRelPath = `${match.path}/SKILL.md`;
|
|
262
|
+
const content = await readFile(path.join(dir, skillRelPath));
|
|
263
|
+
if (content !== null) {
|
|
264
|
+
discoveredMatches.add(skillRelPath);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const matches = [...discoveredMatches].map(path => ({ path }));
|
|
268
|
+
if (matches.length === 0) {
|
|
269
|
+
return { items, warnings };
|
|
270
|
+
}
|
|
239
271
|
|
|
240
|
-
//
|
|
241
|
-
const ig = createIgnoreMatcher();
|
|
242
|
-
await addIgnoreRules(ig, dir, dir, readFile);
|
|
243
|
-
|
|
244
|
-
const entries = await readDirEntries(dir);
|
|
245
|
-
const skillDirs = entries.filter(
|
|
246
|
-
entry => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules",
|
|
247
|
-
);
|
|
248
|
-
|
|
272
|
+
// Read all skill files in parallel
|
|
249
273
|
const results = await Promise.all(
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
// Check if this directory should be ignored
|
|
254
|
-
if (shouldIgnore(ig, dir, entryPath, true)) {
|
|
255
|
-
return { item: null as Skill | null, warning: null as string | null };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const skillFile = path.join(entryPath, "SKILL.md");
|
|
274
|
+
matches.map(async match => {
|
|
275
|
+
const skillFile = path.join(dir, match.path);
|
|
259
276
|
const content = await readFile(skillFile);
|
|
260
277
|
if (!content) {
|
|
261
278
|
return { item: null as Skill | null, warning: null as string | null };
|
|
262
279
|
}
|
|
263
|
-
|
|
264
280
|
const { frontmatter, body } = parseFrontmatter(content, { source: skillFile });
|
|
265
281
|
if (requireDescription && !frontmatter.description) {
|
|
266
282
|
return { item: null as Skill | null, warning: null as string | null };
|
|
267
283
|
}
|
|
268
284
|
|
|
285
|
+
// Extract skill name from path: "<skilldir>/SKILL.md" -> "<skilldir>"
|
|
286
|
+
const skillDirName = path.basename(path.dirname(skillFile));
|
|
269
287
|
return {
|
|
270
288
|
item: {
|
|
271
|
-
name: (frontmatter.name as string) ||
|
|
289
|
+
name: (frontmatter.name as string) || skillDirName,
|
|
272
290
|
path: skillFile,
|
|
273
291
|
content: body,
|
|
274
292
|
frontmatter: frontmatter as SkillFrontmatter,
|
|
@@ -279,12 +297,10 @@ export async function loadSkillsFromDir(
|
|
|
279
297
|
};
|
|
280
298
|
}),
|
|
281
299
|
);
|
|
282
|
-
|
|
283
300
|
for (const result of results) {
|
|
284
301
|
if (result.warning) warnings.push(result.warning);
|
|
285
302
|
if (result.item) items.push(result.item);
|
|
286
303
|
}
|
|
287
|
-
|
|
288
304
|
return { items, warnings };
|
|
289
305
|
}
|
|
290
306
|
|
|
@@ -322,8 +338,8 @@ export function expandEnvVarsDeep<T>(obj: T, extraEnv?: Record<string, string>):
|
|
|
322
338
|
}
|
|
323
339
|
|
|
324
340
|
/**
|
|
325
|
-
* Load files from a directory matching
|
|
326
|
-
*
|
|
341
|
+
* Load files from a directory matching extensions.
|
|
342
|
+
* Uses native glob for fast filesystem scanning with gitignore support.
|
|
327
343
|
*/
|
|
328
344
|
export async function loadFilesFromDir<T>(
|
|
329
345
|
_ctx: LoadContext,
|
|
@@ -335,85 +351,70 @@ export async function loadFilesFromDir<T>(
|
|
|
335
351
|
extensions?: string[];
|
|
336
352
|
/** Transform file to item (return null to skip) */
|
|
337
353
|
transform: (name: string, content: string, path: string, source: SourceMeta) => T | null;
|
|
338
|
-
/** Whether to recurse into subdirectories */
|
|
354
|
+
/** Whether to recurse into subdirectories (default: false) */
|
|
339
355
|
recursive?: boolean;
|
|
340
|
-
/** Root directory for ignore file handling (
|
|
356
|
+
/** Root directory for ignore file handling (unused, kept for API compat) */
|
|
341
357
|
rootDir?: string;
|
|
342
|
-
/** Ignore matcher (
|
|
358
|
+
/** Ignore matcher (unused, kept for API compat) */
|
|
343
359
|
ignoreMatcher?: IgnoreMatcher;
|
|
344
360
|
},
|
|
345
361
|
): Promise<LoadResult<T>> {
|
|
346
|
-
const rootDir = options.rootDir ?? dir;
|
|
347
|
-
const ig = options.ignoreMatcher ?? createIgnoreMatcher();
|
|
348
|
-
|
|
349
|
-
// Read ignore rules from this directory
|
|
350
|
-
await addIgnoreRules(ig, dir, rootDir, readFile);
|
|
351
|
-
|
|
352
|
-
const entries = await readDirEntries(dir);
|
|
353
|
-
|
|
354
|
-
const visibleEntries = entries.filter(entry => !entry.name.startsWith("."));
|
|
355
|
-
|
|
356
|
-
const directories = options.recursive
|
|
357
|
-
? visibleEntries.filter(entry => {
|
|
358
|
-
if (!entry.isDirectory()) return false;
|
|
359
|
-
const entryPath = path.join(dir, entry.name);
|
|
360
|
-
return !shouldIgnore(ig, rootDir, entryPath, true);
|
|
361
|
-
})
|
|
362
|
-
: [];
|
|
363
|
-
|
|
364
|
-
const files = visibleEntries.filter(entry => {
|
|
365
|
-
if (!entry.isFile()) return false;
|
|
366
|
-
const entryPath = path.join(dir, entry.name);
|
|
367
|
-
if (shouldIgnore(ig, rootDir, entryPath, false)) return false;
|
|
368
|
-
if (!options.extensions) return true;
|
|
369
|
-
return options.extensions.some(ext => entry.name.endsWith(`.${ext}`));
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
const [subResults, fileResults] = await Promise.all([
|
|
373
|
-
Promise.all(
|
|
374
|
-
directories.map(entry =>
|
|
375
|
-
loadFilesFromDir(_ctx, path.join(dir, entry.name), provider, level, {
|
|
376
|
-
...options,
|
|
377
|
-
rootDir,
|
|
378
|
-
ignoreMatcher: ig,
|
|
379
|
-
}),
|
|
380
|
-
),
|
|
381
|
-
),
|
|
382
|
-
Promise.all(
|
|
383
|
-
files.map(async entry => {
|
|
384
|
-
const filePath = path.join(dir, entry.name);
|
|
385
|
-
const content = await readFile(filePath);
|
|
386
|
-
return { entry, path: filePath, content };
|
|
387
|
-
}),
|
|
388
|
-
),
|
|
389
|
-
]);
|
|
390
|
-
|
|
391
362
|
const items: T[] = [];
|
|
392
363
|
const warnings: string[] = [];
|
|
364
|
+
// Build glob pattern based on extensions and recursion
|
|
365
|
+
const { extensions, recursive = false } = options;
|
|
393
366
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
367
|
+
let pattern: string;
|
|
368
|
+
if (extensions && extensions.length > 0) {
|
|
369
|
+
const extPattern = extensions.length === 1 ? extensions[0] : `{${extensions.join(",")}}`;
|
|
370
|
+
pattern = recursive ? `**/*.${extPattern}` : `*.${extPattern}`;
|
|
371
|
+
} else {
|
|
372
|
+
pattern = recursive ? "**/*" : "*";
|
|
397
373
|
}
|
|
398
374
|
|
|
399
|
-
|
|
375
|
+
// Use native glob for fast scanning with gitignore support
|
|
376
|
+
let matches: Array<{ path: string }>;
|
|
377
|
+
try {
|
|
378
|
+
const result = await glob({
|
|
379
|
+
pattern,
|
|
380
|
+
path: dir,
|
|
381
|
+
gitignore: true,
|
|
382
|
+
hidden: false,
|
|
383
|
+
fileType: FileType.File,
|
|
384
|
+
});
|
|
385
|
+
matches = result.matches;
|
|
386
|
+
} catch {
|
|
387
|
+
// Directory doesn't exist or isn't readable
|
|
388
|
+
return { items, warnings };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Read all matching files in parallel
|
|
392
|
+
const fileResults = await Promise.all(
|
|
393
|
+
matches.map(async match => {
|
|
394
|
+
const filePath = path.join(dir, match.path);
|
|
395
|
+
const content = await readFile(filePath);
|
|
396
|
+
return { filePath, content };
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
for (const { filePath, content } of fileResults) {
|
|
400
401
|
if (content === null) {
|
|
401
|
-
warnings.push(`Failed to read file: ${
|
|
402
|
+
warnings.push(`Failed to read file: ${filePath}`);
|
|
402
403
|
continue;
|
|
403
404
|
}
|
|
404
405
|
|
|
405
|
-
const
|
|
406
|
+
const name = path.basename(filePath);
|
|
407
|
+
const source = createSourceMeta(provider, filePath, level);
|
|
406
408
|
|
|
407
409
|
try {
|
|
408
|
-
const item = options.transform(
|
|
410
|
+
const item = options.transform(name, content, filePath, source);
|
|
409
411
|
if (item !== null) {
|
|
410
412
|
items.push(item);
|
|
411
413
|
}
|
|
412
414
|
} catch (err) {
|
|
413
|
-
warnings.push(`Failed to parse ${
|
|
415
|
+
warnings.push(`Failed to parse ${filePath}: ${err}`);
|
|
414
416
|
}
|
|
415
417
|
}
|
|
416
|
-
|
|
417
418
|
return { items, warnings };
|
|
418
419
|
}
|
|
419
420
|
|
|
@@ -458,10 +459,6 @@ async function readExtensionModuleManifest(
|
|
|
458
459
|
return null;
|
|
459
460
|
}
|
|
460
461
|
|
|
461
|
-
function isExtensionModuleFile(name: string): boolean {
|
|
462
|
-
return name.endsWith(".ts") || name.endsWith(".js");
|
|
463
|
-
}
|
|
464
|
-
|
|
465
462
|
/**
|
|
466
463
|
* Discover extension module entry points in a directory.
|
|
467
464
|
*
|
|
@@ -471,61 +468,58 @@ function isExtensionModuleFile(name: string): boolean {
|
|
|
471
468
|
* 3. Subdirectory with package.json: `extensions/<ext>/package.json` with "omp"/"pi" field → load declared paths
|
|
472
469
|
*
|
|
473
470
|
* No recursion beyond one level. Complex packages must use package.json manifest.
|
|
474
|
-
*
|
|
471
|
+
* Uses native glob for fast filesystem scanning with gitignore support.
|
|
475
472
|
*/
|
|
476
|
-
export async function discoverExtensionModulePaths(
|
|
477
|
-
const discovered
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const entryPath = path.join(dir, entry.name);
|
|
488
|
-
|
|
489
|
-
// 1. Direct files: *.ts or *.js
|
|
490
|
-
if ((entry.isFile() || entry.isSymbolicLink()) && isExtensionModuleFile(entry.name)) {
|
|
491
|
-
if (shouldIgnore(ig, dir, entryPath, false)) continue;
|
|
492
|
-
discovered.push(entryPath);
|
|
493
|
-
continue;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// 2 & 3. Subdirectories
|
|
497
|
-
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
498
|
-
if (shouldIgnore(ig, dir, entryPath, true)) continue;
|
|
499
|
-
|
|
500
|
-
const subEntries = await readDirEntries(entryPath);
|
|
501
|
-
const subFileNames = new Set(subEntries.filter(e => e.isFile()).map(e => e.name));
|
|
502
|
-
|
|
503
|
-
// Check for package.json with "omp"/"pi" field first
|
|
504
|
-
if (subFileNames.has("package.json")) {
|
|
505
|
-
const packageJsonPath = path.join(entryPath, "package.json");
|
|
506
|
-
const manifest = await readExtensionModuleManifest(ctx, packageJsonPath);
|
|
507
|
-
if (manifest?.extensions && Array.isArray(manifest.extensions)) {
|
|
508
|
-
for (const extPath of manifest.extensions) {
|
|
509
|
-
const resolvedExtPath = path.resolve(entryPath, extPath);
|
|
510
|
-
const content = await readFile(resolvedExtPath);
|
|
511
|
-
if (content !== null) {
|
|
512
|
-
discovered.push(resolvedExtPath);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
473
|
+
export async function discoverExtensionModulePaths(_ctx: LoadContext, dir: string): Promise<string[]> {
|
|
474
|
+
const discovered = new Set<string>();
|
|
475
|
+
// Find all candidate files in parallel using glob
|
|
476
|
+
const [directFiles, indexFiles, packageJsonFiles] = await Promise.all([
|
|
477
|
+
// 1. Direct *.ts or *.js files
|
|
478
|
+
globIf(dir, "*.{ts,js}", FileType.File, false),
|
|
479
|
+
// 2. Subdirectory index files
|
|
480
|
+
globIf(dir, "*/index.{ts,js}", FileType.File),
|
|
481
|
+
// 3. Subdirectory package.json files
|
|
482
|
+
globIf(dir, "*/package.json", FileType.File),
|
|
483
|
+
]);
|
|
518
484
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
485
|
+
// Process direct files
|
|
486
|
+
for (const match of directFiles) {
|
|
487
|
+
if (match.path.includes("/")) continue;
|
|
488
|
+
discovered.add(path.join(dir, match.path));
|
|
489
|
+
}
|
|
490
|
+
// Track which subdirectories have package.json manifests with declared extensions
|
|
491
|
+
const subdirsWithDeclaredExtensions = new Set<string>();
|
|
492
|
+
for (const match of packageJsonFiles) {
|
|
493
|
+
const subdir = path.dirname(match.path); // e.g., "my-extension"
|
|
494
|
+
const packageJsonPath = path.join(dir, match.path);
|
|
495
|
+
const manifest = await readExtensionModuleManifest(_ctx, packageJsonPath);
|
|
496
|
+
const declaredExtensions =
|
|
497
|
+
manifest?.extensions?.filter((extPath): extPath is string => typeof extPath === "string") ?? [];
|
|
498
|
+
if (declaredExtensions.length === 0) continue;
|
|
499
|
+
subdirsWithDeclaredExtensions.add(subdir);
|
|
500
|
+
const subdirPath = path.join(dir, subdir);
|
|
501
|
+
for (const extPath of declaredExtensions) {
|
|
502
|
+
const resolvedExtPath = path.resolve(subdirPath, extPath);
|
|
503
|
+
const content = await readFile(resolvedExtPath);
|
|
504
|
+
if (content !== null) {
|
|
505
|
+
discovered.add(resolvedExtPath);
|
|
524
506
|
}
|
|
525
507
|
}
|
|
526
508
|
}
|
|
527
|
-
|
|
528
|
-
|
|
509
|
+
const preferredIndexBySubdir = new Map<string, string>();
|
|
510
|
+
for (const match of indexFiles) {
|
|
511
|
+
if (match.path.split("/").length !== 2) continue;
|
|
512
|
+
const subdir = path.dirname(match.path);
|
|
513
|
+
if (subdirsWithDeclaredExtensions.has(subdir)) continue;
|
|
514
|
+
const existing = preferredIndexBySubdir.get(subdir);
|
|
515
|
+
if (!existing || (existing.endsWith("index.js") && match.path.endsWith("index.ts"))) {
|
|
516
|
+
preferredIndexBySubdir.set(subdir, match.path);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (const preferredPath of preferredIndexBySubdir.values()) {
|
|
520
|
+
discovered.add(path.join(dir, preferredPath));
|
|
521
|
+
}
|
|
522
|
+
return [...discovered];
|
|
529
523
|
}
|
|
530
524
|
|
|
531
525
|
/**
|
|
@@ -8,11 +8,14 @@ import type { AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agen
|
|
|
8
8
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
10
10
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
11
|
+
import type { Rule } from "../../capability/rule";
|
|
11
12
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
12
13
|
import type { ExecOptions, ExecResult } from "../../exec/exec";
|
|
13
14
|
import type { HookUIContext } from "../../extensibility/hooks/types";
|
|
14
15
|
import type { Theme } from "../../modes/theme/theme";
|
|
16
|
+
import type { CompactionResult } from "../../session/compaction";
|
|
15
17
|
import type { ReadonlySessionManager } from "../../session/session-manager";
|
|
18
|
+
import type { TodoItem } from "../../tools/todo-write";
|
|
16
19
|
|
|
17
20
|
/** Alias for clarity */
|
|
18
21
|
export type CustomToolUIContext = HookUIContext;
|
|
@@ -61,12 +64,47 @@ export interface CustomToolContext {
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/** Session event passed to onSession callback */
|
|
64
|
-
export
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
export type CustomToolSessionEvent =
|
|
68
|
+
| {
|
|
69
|
+
/** Reason for the session event */
|
|
70
|
+
reason: "start" | "switch" | "branch" | "tree" | "shutdown";
|
|
71
|
+
/** Previous session file path, or undefined for "start" and "shutdown" */
|
|
72
|
+
previousSessionFile: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
reason: "auto_compaction_start";
|
|
76
|
+
trigger: "threshold" | "overflow";
|
|
77
|
+
}
|
|
78
|
+
| {
|
|
79
|
+
reason: "auto_compaction_end";
|
|
80
|
+
result: CompactionResult | undefined;
|
|
81
|
+
aborted: boolean;
|
|
82
|
+
willRetry: boolean;
|
|
83
|
+
errorMessage?: string;
|
|
84
|
+
}
|
|
85
|
+
| {
|
|
86
|
+
reason: "auto_retry_start";
|
|
87
|
+
attempt: number;
|
|
88
|
+
maxAttempts: number;
|
|
89
|
+
delayMs: number;
|
|
90
|
+
errorMessage: string;
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
reason: "auto_retry_end";
|
|
94
|
+
success: boolean;
|
|
95
|
+
attempt: number;
|
|
96
|
+
finalError?: string;
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
reason: "ttsr_triggered";
|
|
100
|
+
rules: Rule[];
|
|
101
|
+
}
|
|
102
|
+
| {
|
|
103
|
+
reason: "todo_reminder";
|
|
104
|
+
todos: TodoItem[];
|
|
105
|
+
attempt: number;
|
|
106
|
+
maxAttempts: number;
|
|
107
|
+
};
|
|
70
108
|
|
|
71
109
|
/** Rendering options passed to renderResult */
|
|
72
110
|
export interface RenderResultOptions {
|
|
@@ -12,6 +12,7 @@ import type { ImageContent, Model, TextContent, ToolResultMessage } from "@oh-my
|
|
|
12
12
|
import type * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
|
|
13
13
|
import type { AutocompleteItem, Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
|
|
14
14
|
import type { Static, TSchema } from "@sinclair/typebox";
|
|
15
|
+
import type { Rule } from "../../capability/rule";
|
|
15
16
|
import type { KeybindingsManager } from "../../config/keybindings";
|
|
16
17
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
17
18
|
import type { BashResult } from "../../exec/bash-executor";
|
|
@@ -39,6 +40,7 @@ import type {
|
|
|
39
40
|
ReadToolInput,
|
|
40
41
|
WriteToolInput,
|
|
41
42
|
} from "../../tools";
|
|
43
|
+
import type { TodoItem } from "../../tools/todo-write";
|
|
42
44
|
import type { EventBus } from "../../utils/event-bus";
|
|
43
45
|
import type { SlashCommandInfo } from "../slash-commands";
|
|
44
46
|
|
|
@@ -459,6 +461,52 @@ export interface TurnEndEvent {
|
|
|
459
461
|
toolResults: ToolResultMessage[];
|
|
460
462
|
}
|
|
461
463
|
|
|
464
|
+
/** Fired when auto-compaction starts */
|
|
465
|
+
export interface AutoCompactionStartEvent {
|
|
466
|
+
type: "auto_compaction_start";
|
|
467
|
+
reason: "threshold" | "overflow";
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Fired when auto-compaction ends */
|
|
471
|
+
export interface AutoCompactionEndEvent {
|
|
472
|
+
type: "auto_compaction_end";
|
|
473
|
+
result: CompactionResult | undefined;
|
|
474
|
+
aborted: boolean;
|
|
475
|
+
willRetry: boolean;
|
|
476
|
+
errorMessage?: string;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/** Fired when auto-retry starts */
|
|
480
|
+
export interface AutoRetryStartEvent {
|
|
481
|
+
type: "auto_retry_start";
|
|
482
|
+
attempt: number;
|
|
483
|
+
maxAttempts: number;
|
|
484
|
+
delayMs: number;
|
|
485
|
+
errorMessage: string;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** Fired when auto-retry ends */
|
|
489
|
+
export interface AutoRetryEndEvent {
|
|
490
|
+
type: "auto_retry_end";
|
|
491
|
+
success: boolean;
|
|
492
|
+
attempt: number;
|
|
493
|
+
finalError?: string;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Fired when TTSR rule matching interrupts generation */
|
|
497
|
+
export interface TtsrTriggeredEvent {
|
|
498
|
+
type: "ttsr_triggered";
|
|
499
|
+
rules: Rule[];
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** Fired when todo reminder logic detects unfinished todos */
|
|
503
|
+
export interface TodoReminderEvent {
|
|
504
|
+
type: "todo_reminder";
|
|
505
|
+
todos: TodoItem[];
|
|
506
|
+
attempt: number;
|
|
507
|
+
maxAttempts: number;
|
|
508
|
+
}
|
|
509
|
+
|
|
462
510
|
// ============================================================================
|
|
463
511
|
// User Bash Events
|
|
464
512
|
// ============================================================================
|
|
@@ -652,6 +700,12 @@ export type ExtensionEvent =
|
|
|
652
700
|
| AgentEndEvent
|
|
653
701
|
| TurnStartEvent
|
|
654
702
|
| TurnEndEvent
|
|
703
|
+
| AutoCompactionStartEvent
|
|
704
|
+
| AutoCompactionEndEvent
|
|
705
|
+
| AutoRetryStartEvent
|
|
706
|
+
| AutoRetryEndEvent
|
|
707
|
+
| TtsrTriggeredEvent
|
|
708
|
+
| TodoReminderEvent
|
|
655
709
|
| UserBashEvent
|
|
656
710
|
| UserPythonEvent
|
|
657
711
|
| InputEvent
|
|
@@ -814,6 +868,12 @@ export interface ExtensionAPI {
|
|
|
814
868
|
on(event: "agent_end", handler: ExtensionHandler<AgentEndEvent>): void;
|
|
815
869
|
on(event: "turn_start", handler: ExtensionHandler<TurnStartEvent>): void;
|
|
816
870
|
on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
|
|
871
|
+
on(event: "auto_compaction_start", handler: ExtensionHandler<AutoCompactionStartEvent>): void;
|
|
872
|
+
on(event: "auto_compaction_end", handler: ExtensionHandler<AutoCompactionEndEvent>): void;
|
|
873
|
+
on(event: "auto_retry_start", handler: ExtensionHandler<AutoRetryStartEvent>): void;
|
|
874
|
+
on(event: "auto_retry_end", handler: ExtensionHandler<AutoRetryEndEvent>): void;
|
|
875
|
+
on(event: "ttsr_triggered", handler: ExtensionHandler<TtsrTriggeredEvent>): void;
|
|
876
|
+
on(event: "todo_reminder", handler: ExtensionHandler<TodoReminderEvent>): void;
|
|
817
877
|
on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
|
|
818
878
|
on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
|
|
819
879
|
on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
|