@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
package/src/sdk.ts
CHANGED
|
@@ -441,6 +441,58 @@ function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
|
|
|
441
441
|
api.on("session_shutdown", async (_event, ctx) =>
|
|
442
442
|
runOnSession({ reason: "shutdown", previousSessionFile: undefined }, ctx),
|
|
443
443
|
);
|
|
444
|
+
api.on("auto_compaction_start", async (event, ctx) =>
|
|
445
|
+
runOnSession({ reason: "auto_compaction_start", trigger: event.reason }, ctx),
|
|
446
|
+
);
|
|
447
|
+
api.on("auto_compaction_end", async (event, ctx) =>
|
|
448
|
+
runOnSession(
|
|
449
|
+
{
|
|
450
|
+
reason: "auto_compaction_end",
|
|
451
|
+
result: event.result,
|
|
452
|
+
aborted: event.aborted,
|
|
453
|
+
willRetry: event.willRetry,
|
|
454
|
+
errorMessage: event.errorMessage,
|
|
455
|
+
},
|
|
456
|
+
ctx,
|
|
457
|
+
),
|
|
458
|
+
);
|
|
459
|
+
api.on("auto_retry_start", async (event, ctx) =>
|
|
460
|
+
runOnSession(
|
|
461
|
+
{
|
|
462
|
+
reason: "auto_retry_start",
|
|
463
|
+
attempt: event.attempt,
|
|
464
|
+
maxAttempts: event.maxAttempts,
|
|
465
|
+
delayMs: event.delayMs,
|
|
466
|
+
errorMessage: event.errorMessage,
|
|
467
|
+
},
|
|
468
|
+
ctx,
|
|
469
|
+
),
|
|
470
|
+
);
|
|
471
|
+
api.on("auto_retry_end", async (event, ctx) =>
|
|
472
|
+
runOnSession(
|
|
473
|
+
{
|
|
474
|
+
reason: "auto_retry_end",
|
|
475
|
+
success: event.success,
|
|
476
|
+
attempt: event.attempt,
|
|
477
|
+
finalError: event.finalError,
|
|
478
|
+
},
|
|
479
|
+
ctx,
|
|
480
|
+
),
|
|
481
|
+
);
|
|
482
|
+
api.on("ttsr_triggered", async (event, ctx) =>
|
|
483
|
+
runOnSession({ reason: "ttsr_triggered", rules: event.rules }, ctx),
|
|
484
|
+
);
|
|
485
|
+
api.on("todo_reminder", async (event, ctx) =>
|
|
486
|
+
runOnSession(
|
|
487
|
+
{
|
|
488
|
+
reason: "todo_reminder",
|
|
489
|
+
todos: event.todos,
|
|
490
|
+
attempt: event.attempt,
|
|
491
|
+
maxAttempts: event.maxAttempts,
|
|
492
|
+
},
|
|
493
|
+
ctx,
|
|
494
|
+
),
|
|
495
|
+
);
|
|
444
496
|
};
|
|
445
497
|
}
|
|
446
498
|
|
|
@@ -496,6 +548,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
496
548
|
time("settings");
|
|
497
549
|
initializeWithSettings(settings);
|
|
498
550
|
time("initializeWithSettings");
|
|
551
|
+
const skillsSettings = settings.getGroup("skills") as SkillsSettings;
|
|
552
|
+
const discoveredSkillsPromise =
|
|
553
|
+
options.skills === undefined ? discoverSkills(cwd, agentDir, skillsSettings) : undefined;
|
|
499
554
|
|
|
500
555
|
// Initialize provider preferences from settings
|
|
501
556
|
setPreferredSearchProvider(settings.get("providers.webSearch") ?? "auto");
|
|
@@ -504,6 +559,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
504
559
|
const sessionManager = options.sessionManager ?? SessionManager.create(cwd);
|
|
505
560
|
time("sessionManager");
|
|
506
561
|
const sessionId = sessionManager.getSessionId();
|
|
562
|
+
const modelApiKeyAvailability = new Map<string, boolean>();
|
|
563
|
+
const getModelAvailabilityKey = (candidate: Model): string =>
|
|
564
|
+
`${candidate.provider}\u0000${candidate.baseUrl ?? ""}`;
|
|
565
|
+
const hasModelApiKey = async (candidate: Model): Promise<boolean> => {
|
|
566
|
+
const availabilityKey = getModelAvailabilityKey(candidate);
|
|
567
|
+
const cached = modelApiKeyAvailability.get(availabilityKey);
|
|
568
|
+
if (cached !== undefined) {
|
|
569
|
+
return cached;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const hasKey = !!(await modelRegistry.getApiKey(candidate, sessionId));
|
|
573
|
+
modelApiKeyAvailability.set(availabilityKey, hasKey);
|
|
574
|
+
return hasKey;
|
|
575
|
+
};
|
|
507
576
|
|
|
508
577
|
// Check if session has existing data to restore
|
|
509
578
|
const existingSession = sessionManager.buildSessionContext();
|
|
@@ -521,7 +590,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
521
590
|
const parsedModel = parseModelString(defaultModelStr);
|
|
522
591
|
if (parsedModel) {
|
|
523
592
|
const restoredModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
524
|
-
if (restoredModel && (await
|
|
593
|
+
if (restoredModel && (await hasModelApiKey(restoredModel))) {
|
|
525
594
|
model = restoredModel;
|
|
526
595
|
}
|
|
527
596
|
}
|
|
@@ -537,7 +606,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
537
606
|
const parsedModel = parseModelString(settingsDefaultModel);
|
|
538
607
|
if (parsedModel) {
|
|
539
608
|
const settingsModel = modelRegistry.find(parsedModel.provider, parsedModel.id);
|
|
540
|
-
if (settingsModel && (await
|
|
609
|
+
if (settingsModel && (await hasModelApiKey(settingsModel))) {
|
|
541
610
|
model = settingsModel;
|
|
542
611
|
}
|
|
543
612
|
}
|
|
@@ -547,10 +616,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
547
616
|
// Fall back to first available model with a valid API key
|
|
548
617
|
if (!model) {
|
|
549
618
|
const allModels = modelRegistry.getAll();
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
619
|
+
for (const candidate of allModels) {
|
|
620
|
+
if (await hasModelApiKey(candidate)) {
|
|
621
|
+
model = candidate;
|
|
622
|
+
break;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
554
625
|
time("findAvailableModel");
|
|
555
626
|
if (model) {
|
|
556
627
|
if (modelFallbackMessage) {
|
|
@@ -563,6 +634,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
563
634
|
}
|
|
564
635
|
}
|
|
565
636
|
|
|
637
|
+
time("findModel");
|
|
638
|
+
|
|
566
639
|
// For subagent sessions using GitHub Copilot, add X-Initiator header
|
|
567
640
|
// to ensure proper billing (agent-initiated vs user-initiated)
|
|
568
641
|
const taskDepth = options.taskDepth ?? 0;
|
|
@@ -604,12 +677,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
604
677
|
skills = options.skills;
|
|
605
678
|
skillWarnings = [];
|
|
606
679
|
} else {
|
|
607
|
-
const
|
|
608
|
-
|
|
680
|
+
const discovered = discoveredSkillsPromise ? await discoveredSkillsPromise : { skills: [], warnings: [] };
|
|
681
|
+
time("discoverSkills");
|
|
609
682
|
skills = discovered.skills;
|
|
610
683
|
skillWarnings = discovered.warnings;
|
|
611
684
|
}
|
|
612
|
-
|
|
685
|
+
|
|
613
686
|
debugStartup("sdk:discoverSkills");
|
|
614
687
|
|
|
615
688
|
// Discover rules
|
|
@@ -66,7 +66,7 @@ import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
|
66
66
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
67
67
|
import { resolvePlanUrlToPath } from "../internal-urls";
|
|
68
68
|
import { executePython as executePythonCommand, type PythonResult } from "../ipy/executor";
|
|
69
|
-
import { theme } from "../modes/theme/theme";
|
|
69
|
+
import { getCurrentThemeName, theme } from "../modes/theme/theme";
|
|
70
70
|
import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
|
|
71
71
|
import type { PlanModeState } from "../plan-mode/state";
|
|
72
72
|
import planModeActivePrompt from "../prompts/system/plan-mode-active.md" with { type: "text" };
|
|
@@ -340,6 +340,7 @@ export class AgentSession {
|
|
|
340
340
|
#streamingEditCheckedLineCounts = new Map<string, number>();
|
|
341
341
|
#streamingEditFileCache = new Map<string, string>();
|
|
342
342
|
#promptInFlight = false;
|
|
343
|
+
#promptGeneration = 0;
|
|
343
344
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
344
345
|
|
|
345
346
|
constructor(config: AgentSessionConfig) {
|
|
@@ -400,6 +401,11 @@ export class AgentSession {
|
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
|
|
404
|
+
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
405
|
+
await this.#emitExtensionEvent(event);
|
|
406
|
+
this.#emit(event);
|
|
407
|
+
}
|
|
408
|
+
|
|
403
409
|
// Track last assistant message for auto-compaction check
|
|
404
410
|
#lastAssistantMessage: AssistantMessage | undefined = undefined;
|
|
405
411
|
|
|
@@ -424,11 +430,7 @@ export class AgentSession {
|
|
|
424
430
|
}
|
|
425
431
|
}
|
|
426
432
|
|
|
427
|
-
|
|
428
|
-
await this.#emitExtensionEvent(event);
|
|
429
|
-
|
|
430
|
-
// Notify all listeners
|
|
431
|
-
this.#emit(event);
|
|
433
|
+
await this.#emitSessionEvent(event);
|
|
432
434
|
|
|
433
435
|
if (event.type === "turn_start") {
|
|
434
436
|
this.#resetStreamingEditState();
|
|
@@ -453,11 +455,11 @@ export class AgentSession {
|
|
|
453
455
|
this.#ttsrManager.markInjected(matches);
|
|
454
456
|
// Store for injection on retry
|
|
455
457
|
this.#pendingTtsrInjections.push(...matches);
|
|
456
|
-
//
|
|
458
|
+
// Abort the stream immediately — do not gate on extension callbacks
|
|
457
459
|
this.#ttsrAbortPending = true;
|
|
458
|
-
this.#emit({ type: "ttsr_triggered", rules: matches });
|
|
459
|
-
// Abort the stream
|
|
460
460
|
this.agent.abort();
|
|
461
|
+
// Notify extensions (fire-and-forget, does not block abort)
|
|
462
|
+
this.#emitSessionEvent({ type: "ttsr_triggered", rules: matches }).catch(() => {});
|
|
461
463
|
// Schedule retry after a short delay
|
|
462
464
|
setTimeout(async () => {
|
|
463
465
|
this.#ttsrAbortPending = false;
|
|
@@ -525,8 +527,12 @@ export class AgentSession {
|
|
|
525
527
|
// Reset retry counter immediately on successful assistant response
|
|
526
528
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
527
529
|
const assistantMsg = event.message as AssistantMessage;
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
+
if (
|
|
531
|
+
assistantMsg.stopReason !== "error" &&
|
|
532
|
+
assistantMsg.stopReason !== "aborted" &&
|
|
533
|
+
this.#retryAttempt > 0
|
|
534
|
+
) {
|
|
535
|
+
await this.#emitSessionEvent({
|
|
530
536
|
type: "auto_retry_end",
|
|
531
537
|
success: true,
|
|
532
538
|
attempt: this.#retryAttempt,
|
|
@@ -537,11 +543,13 @@ export class AgentSession {
|
|
|
537
543
|
}
|
|
538
544
|
|
|
539
545
|
if (event.message.role === "toolResult") {
|
|
540
|
-
const { toolName, $normative, toolCallId, details } = event.message as {
|
|
546
|
+
const { toolName, $normative, toolCallId, details, isError, content } = event.message as {
|
|
541
547
|
toolName?: string;
|
|
542
548
|
toolCallId?: string;
|
|
543
549
|
details?: { path?: string };
|
|
544
550
|
$normative?: Record<string, unknown>;
|
|
551
|
+
isError?: boolean;
|
|
552
|
+
content?: Array<TextContent | ImageContent>;
|
|
545
553
|
};
|
|
546
554
|
if ($normative && toolCallId && this.settings.get("normativeRewrite")) {
|
|
547
555
|
await this.#rewriteToolCallArgs(toolCallId, $normative);
|
|
@@ -550,6 +558,25 @@ export class AgentSession {
|
|
|
550
558
|
if (toolName === "edit" && details?.path) {
|
|
551
559
|
this.#invalidateFileCacheForPath(details.path);
|
|
552
560
|
}
|
|
561
|
+
if (toolName === "todo_write" && isError) {
|
|
562
|
+
const errorText = content?.find(part => part.type === "text")?.text;
|
|
563
|
+
const reminderText = [
|
|
564
|
+
"<system_reminder>",
|
|
565
|
+
"todo_write failed, so todo progress is not visible to the user.",
|
|
566
|
+
errorText ? `Failure: ${errorText}` : "Failure: todo_write returned an error.",
|
|
567
|
+
"Fix the todo payload and call todo_write again before continuing.",
|
|
568
|
+
"</system_reminder>",
|
|
569
|
+
].join("\n");
|
|
570
|
+
await this.sendCustomMessage(
|
|
571
|
+
{
|
|
572
|
+
customType: "todo-write-error-reminder",
|
|
573
|
+
content: reminderText,
|
|
574
|
+
display: false,
|
|
575
|
+
details: { toolName, errorText },
|
|
576
|
+
},
|
|
577
|
+
{ deliverAs: "nextTurn" },
|
|
578
|
+
);
|
|
579
|
+
}
|
|
553
580
|
}
|
|
554
581
|
}
|
|
555
582
|
|
|
@@ -823,10 +850,9 @@ export class AgentSession {
|
|
|
823
850
|
}
|
|
824
851
|
}
|
|
825
852
|
|
|
826
|
-
/** Emit extension events based on
|
|
827
|
-
async #emitExtensionEvent(event:
|
|
853
|
+
/** Emit extension events based on session events */
|
|
854
|
+
async #emitExtensionEvent(event: AgentSessionEvent): Promise<void> {
|
|
828
855
|
if (!this.#extensionRunner) return;
|
|
829
|
-
|
|
830
856
|
if (event.type === "agent_start") {
|
|
831
857
|
this.#turnIndex = 0;
|
|
832
858
|
await this.#extensionRunner.emit({ type: "agent_start" });
|
|
@@ -848,6 +874,40 @@ export class AgentSession {
|
|
|
848
874
|
};
|
|
849
875
|
await this.#extensionRunner.emit(hookEvent);
|
|
850
876
|
this.#turnIndex++;
|
|
877
|
+
} else if (event.type === "auto_compaction_start") {
|
|
878
|
+
await this.#extensionRunner.emit({ type: "auto_compaction_start", reason: event.reason });
|
|
879
|
+
} else if (event.type === "auto_compaction_end") {
|
|
880
|
+
await this.#extensionRunner.emit({
|
|
881
|
+
type: "auto_compaction_end",
|
|
882
|
+
result: event.result,
|
|
883
|
+
aborted: event.aborted,
|
|
884
|
+
willRetry: event.willRetry,
|
|
885
|
+
errorMessage: event.errorMessage,
|
|
886
|
+
});
|
|
887
|
+
} else if (event.type === "auto_retry_start") {
|
|
888
|
+
await this.#extensionRunner.emit({
|
|
889
|
+
type: "auto_retry_start",
|
|
890
|
+
attempt: event.attempt,
|
|
891
|
+
maxAttempts: event.maxAttempts,
|
|
892
|
+
delayMs: event.delayMs,
|
|
893
|
+
errorMessage: event.errorMessage,
|
|
894
|
+
});
|
|
895
|
+
} else if (event.type === "auto_retry_end") {
|
|
896
|
+
await this.#extensionRunner.emit({
|
|
897
|
+
type: "auto_retry_end",
|
|
898
|
+
success: event.success,
|
|
899
|
+
attempt: event.attempt,
|
|
900
|
+
finalError: event.finalError,
|
|
901
|
+
});
|
|
902
|
+
} else if (event.type === "ttsr_triggered") {
|
|
903
|
+
await this.#extensionRunner.emit({ type: "ttsr_triggered", rules: event.rules });
|
|
904
|
+
} else if (event.type === "todo_reminder") {
|
|
905
|
+
await this.#extensionRunner.emit({
|
|
906
|
+
type: "todo_reminder",
|
|
907
|
+
todos: event.todos,
|
|
908
|
+
attempt: event.attempt,
|
|
909
|
+
maxAttempts: event.maxAttempts,
|
|
910
|
+
});
|
|
851
911
|
}
|
|
852
912
|
}
|
|
853
913
|
|
|
@@ -1342,6 +1402,7 @@ export class AgentSession {
|
|
|
1342
1402
|
options?: Pick<PromptOptions, "toolChoice" | "images">,
|
|
1343
1403
|
): Promise<void> {
|
|
1344
1404
|
this.#promptInFlight = true;
|
|
1405
|
+
const generation = this.#promptGeneration;
|
|
1345
1406
|
try {
|
|
1346
1407
|
// Flush any pending bash messages before the new prompt
|
|
1347
1408
|
this.#flushPendingBashMessages();
|
|
@@ -1387,6 +1448,12 @@ export class AgentSession {
|
|
|
1387
1448
|
|
|
1388
1449
|
messages.push(message);
|
|
1389
1450
|
|
|
1451
|
+
// Early bail-out: if a newer abort/prompt cycle started during setup,
|
|
1452
|
+
// return before mutating shared state (nextTurn messages, system prompt).
|
|
1453
|
+
if (this.#promptGeneration !== generation) {
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1390
1457
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
1391
1458
|
for (const msg of this.#pendingNextTurnMessages) {
|
|
1392
1459
|
messages.push(msg);
|
|
@@ -1430,6 +1497,11 @@ export class AgentSession {
|
|
|
1430
1497
|
}
|
|
1431
1498
|
}
|
|
1432
1499
|
|
|
1500
|
+
// Bail out if a newer abort/prompt cycle has started since we began setup
|
|
1501
|
+
if (this.#promptGeneration !== generation) {
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1433
1505
|
const agentPromptOptions = options?.toolChoice ? { toolChoice: options.toolChoice } : undefined;
|
|
1434
1506
|
await this.agent.prompt(messages, agentPromptOptions);
|
|
1435
1507
|
await this.#waitForRetry();
|
|
@@ -1797,8 +1869,14 @@ export class AgentSession {
|
|
|
1797
1869
|
*/
|
|
1798
1870
|
async abort(): Promise<void> {
|
|
1799
1871
|
this.abortRetry();
|
|
1872
|
+
this.#promptGeneration++;
|
|
1800
1873
|
this.agent.abort();
|
|
1801
1874
|
await this.agent.waitForIdle();
|
|
1875
|
+
// Clear promptInFlight: waitForIdle resolves when the agent loop's finally
|
|
1876
|
+
// block runs (#resolveRunningPrompt), but #promptWithMessage's finally
|
|
1877
|
+
// (#promptInFlight = false) fires on a later microtask. Without this,
|
|
1878
|
+
// isStreaming stays true and a subsequent prompt() throws.
|
|
1879
|
+
this.#promptInFlight = false;
|
|
1802
1880
|
}
|
|
1803
1881
|
|
|
1804
1882
|
/**
|
|
@@ -2672,7 +2750,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2672
2750
|
});
|
|
2673
2751
|
|
|
2674
2752
|
// Emit event for UI to render notification
|
|
2675
|
-
this.#
|
|
2753
|
+
await this.#emitSessionEvent({
|
|
2676
2754
|
type: "todo_reminder",
|
|
2677
2755
|
todos: incomplete,
|
|
2678
2756
|
attempt: this.#todoReminderCount,
|
|
@@ -2743,7 +2821,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2743
2821
|
async #runAutoCompaction(reason: "overflow" | "threshold", willRetry: boolean): Promise<void> {
|
|
2744
2822
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
2745
2823
|
|
|
2746
|
-
this.#
|
|
2824
|
+
await this.#emitSessionEvent({ type: "auto_compaction_start", reason });
|
|
2747
2825
|
// Properly abort and null existing controller before replacing
|
|
2748
2826
|
if (this.#autoCompactionAbortController) {
|
|
2749
2827
|
this.#autoCompactionAbortController.abort();
|
|
@@ -2752,13 +2830,23 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2752
2830
|
|
|
2753
2831
|
try {
|
|
2754
2832
|
if (!this.model) {
|
|
2755
|
-
this.#
|
|
2833
|
+
await this.#emitSessionEvent({
|
|
2834
|
+
type: "auto_compaction_end",
|
|
2835
|
+
result: undefined,
|
|
2836
|
+
aborted: false,
|
|
2837
|
+
willRetry: false,
|
|
2838
|
+
});
|
|
2756
2839
|
return;
|
|
2757
2840
|
}
|
|
2758
2841
|
|
|
2759
2842
|
const availableModels = this.#modelRegistry.getAvailable();
|
|
2760
2843
|
if (availableModels.length === 0) {
|
|
2761
|
-
this.#
|
|
2844
|
+
await this.#emitSessionEvent({
|
|
2845
|
+
type: "auto_compaction_end",
|
|
2846
|
+
result: undefined,
|
|
2847
|
+
aborted: false,
|
|
2848
|
+
willRetry: false,
|
|
2849
|
+
});
|
|
2762
2850
|
return;
|
|
2763
2851
|
}
|
|
2764
2852
|
|
|
@@ -2766,7 +2854,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2766
2854
|
|
|
2767
2855
|
const preparation = prepareCompaction(pathEntries, compactionSettings);
|
|
2768
2856
|
if (!preparation) {
|
|
2769
|
-
this.#
|
|
2857
|
+
await this.#emitSessionEvent({
|
|
2858
|
+
type: "auto_compaction_end",
|
|
2859
|
+
result: undefined,
|
|
2860
|
+
aborted: false,
|
|
2861
|
+
willRetry: false,
|
|
2862
|
+
});
|
|
2770
2863
|
return;
|
|
2771
2864
|
}
|
|
2772
2865
|
|
|
@@ -2786,7 +2879,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2786
2879
|
})) as SessionBeforeCompactResult | undefined;
|
|
2787
2880
|
|
|
2788
2881
|
if (hookResult?.cancel) {
|
|
2789
|
-
this.#
|
|
2882
|
+
await this.#emitSessionEvent({
|
|
2883
|
+
type: "auto_compaction_end",
|
|
2884
|
+
result: undefined,
|
|
2885
|
+
aborted: true,
|
|
2886
|
+
willRetry: false,
|
|
2887
|
+
});
|
|
2790
2888
|
return;
|
|
2791
2889
|
}
|
|
2792
2890
|
|
|
@@ -2914,7 +3012,12 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2914
3012
|
}
|
|
2915
3013
|
|
|
2916
3014
|
if (this.#autoCompactionAbortController.signal.aborted) {
|
|
2917
|
-
this.#
|
|
3015
|
+
await this.#emitSessionEvent({
|
|
3016
|
+
type: "auto_compaction_end",
|
|
3017
|
+
result: undefined,
|
|
3018
|
+
aborted: true,
|
|
3019
|
+
willRetry: false,
|
|
3020
|
+
});
|
|
2918
3021
|
return;
|
|
2919
3022
|
}
|
|
2920
3023
|
|
|
@@ -2952,7 +3055,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2952
3055
|
details,
|
|
2953
3056
|
preserveData,
|
|
2954
3057
|
};
|
|
2955
|
-
this.#
|
|
3058
|
+
await this.#emitSessionEvent({ type: "auto_compaction_end", result, aborted: false, willRetry });
|
|
2956
3059
|
|
|
2957
3060
|
if (!willRetry && compactionSettings.autoContinue !== false) {
|
|
2958
3061
|
await this.prompt("Continue if you have next steps.", {
|
|
@@ -2980,11 +3083,16 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
2980
3083
|
}
|
|
2981
3084
|
} catch (error) {
|
|
2982
3085
|
if (this.#autoCompactionAbortController?.signal.aborted) {
|
|
2983
|
-
this.#
|
|
3086
|
+
await this.#emitSessionEvent({
|
|
3087
|
+
type: "auto_compaction_end",
|
|
3088
|
+
result: undefined,
|
|
3089
|
+
aborted: true,
|
|
3090
|
+
willRetry: false,
|
|
3091
|
+
});
|
|
2984
3092
|
return;
|
|
2985
3093
|
}
|
|
2986
3094
|
const errorMessage = error instanceof Error ? error.message : "compaction failed";
|
|
2987
|
-
this.#
|
|
3095
|
+
await this.#emitSessionEvent({
|
|
2988
3096
|
type: "auto_compaction_end",
|
|
2989
3097
|
result: undefined,
|
|
2990
3098
|
aborted: false,
|
|
@@ -3106,7 +3214,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3106
3214
|
|
|
3107
3215
|
if (this.#retryAttempt > retrySettings.maxRetries) {
|
|
3108
3216
|
// Max retries exceeded, emit final failure and reset
|
|
3109
|
-
this.#
|
|
3217
|
+
await this.#emitSessionEvent({
|
|
3110
3218
|
type: "auto_retry_end",
|
|
3111
3219
|
success: false,
|
|
3112
3220
|
attempt: this.#retryAttempt - 1,
|
|
@@ -3135,7 +3243,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3135
3243
|
}
|
|
3136
3244
|
}
|
|
3137
3245
|
|
|
3138
|
-
this.#
|
|
3246
|
+
await this.#emitSessionEvent({
|
|
3139
3247
|
type: "auto_retry_start",
|
|
3140
3248
|
attempt: this.#retryAttempt,
|
|
3141
3249
|
maxAttempts: retrySettings.maxRetries,
|
|
@@ -3162,7 +3270,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3162
3270
|
const attempt = this.#retryAttempt;
|
|
3163
3271
|
this.#retryAttempt = 0;
|
|
3164
3272
|
this.#retryAbortController = undefined;
|
|
3165
|
-
this.#
|
|
3273
|
+
await this.#emitSessionEvent({
|
|
3166
3274
|
type: "auto_retry_end",
|
|
3167
3275
|
success: false,
|
|
3168
3276
|
attempt,
|
|
@@ -3932,7 +4040,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3932
4040
|
* @returns Path to exported file
|
|
3933
4041
|
*/
|
|
3934
4042
|
async exportToHtml(outputPath?: string): Promise<string> {
|
|
3935
|
-
const themeName =
|
|
4043
|
+
const themeName = getCurrentThemeName();
|
|
3936
4044
|
return exportSessionToHtml(this.sessionManager, this.state, { outputPath, themeName });
|
|
3937
4045
|
}
|
|
3938
4046
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import { $ } from "bun";
|
|
3
|
+
import { resolvePython } from "./transcriber";
|
|
4
|
+
|
|
5
|
+
export interface DownloadProgress {
|
|
6
|
+
stage: string;
|
|
7
|
+
percent?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface EnsureOptions {
|
|
11
|
+
modelName?: string;
|
|
12
|
+
onProgress?: (progress: DownloadProgress) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ── Recording tool ─────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
async function ensureRecordingTool(options?: EnsureOptions): Promise<void> {
|
|
18
|
+
if (Bun.which("sox")) return;
|
|
19
|
+
if (Bun.which("ffmpeg")) return;
|
|
20
|
+
if (process.platform === "linux" && Bun.which("arecord")) return;
|
|
21
|
+
|
|
22
|
+
// Windows: PowerShell mciSendString is always available as fallback
|
|
23
|
+
if (process.platform === "win32") {
|
|
24
|
+
// Try to get ffmpeg for better quality, but don't block on failure
|
|
25
|
+
options?.onProgress?.({ stage: "Trying to install FFmpeg via winget..." });
|
|
26
|
+
const result = await $`winget install --id Gyan.FFmpeg -e --accept-source-agreements --accept-package-agreements`
|
|
27
|
+
.quiet()
|
|
28
|
+
.nothrow();
|
|
29
|
+
if (result.exitCode === 0) {
|
|
30
|
+
logger.debug("FFmpeg installed via winget");
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
throw new Error(
|
|
36
|
+
"No audio recording tool found. Install SoX: sudo apt install sox, or FFmpeg: sudo apt install ffmpeg",
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Python whisper ─────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
async function ensurePythonWhisper(options?: EnsureOptions): Promise<void> {
|
|
43
|
+
const pythonCmd = resolvePython();
|
|
44
|
+
if (!pythonCmd) {
|
|
45
|
+
throw new Error("Python not found. Install Python 3.8+ from https://python.org");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if whisper module is already importable
|
|
49
|
+
const check = Bun.spawnSync([pythonCmd, "-c", "import whisper"], {
|
|
50
|
+
stdout: "pipe",
|
|
51
|
+
stderr: "pipe",
|
|
52
|
+
});
|
|
53
|
+
if (check.exitCode === 0) return;
|
|
54
|
+
|
|
55
|
+
options?.onProgress?.({ stage: "Installing openai-whisper (this may take a few minutes)..." });
|
|
56
|
+
logger.debug("Installing openai-whisper via pip");
|
|
57
|
+
|
|
58
|
+
const install = await $`${pythonCmd} -m pip install -q openai-whisper`.quiet().nothrow();
|
|
59
|
+
if (install.exitCode !== 0) {
|
|
60
|
+
const stderr = install.stderr.toString().trim();
|
|
61
|
+
throw new Error(`Failed to install openai-whisper: ${stderr.split("\n").pop()}`);
|
|
62
|
+
}
|
|
63
|
+
logger.debug("openai-whisper installed successfully");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ── Public API ─────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export async function ensureSTTDependencies(options?: EnsureOptions): Promise<void> {
|
|
69
|
+
await ensureRecordingTool(options);
|
|
70
|
+
await ensurePythonWhisper(options);
|
|
71
|
+
}
|
package/src/stt/index.ts
ADDED