@oh-my-pi/pi-coding-agent 6.7.670 → 6.8.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 +28 -0
- package/package.json +6 -7
- package/src/cli/session-picker.ts +27 -28
- package/src/cli/setup-cli.ts +7 -16
- package/src/cli/update-cli.ts +1 -1
- package/src/config.ts +1 -1
- package/src/core/agent-session.ts +202 -37
- package/src/core/agent-storage.ts +1 -1
- package/src/core/auth-storage.ts +15 -25
- package/src/core/bash-executor.ts +63 -105
- package/src/core/custom-commands/loader.ts +1 -1
- package/src/core/custom-tools/loader.ts +1 -1
- package/src/core/custom-tools/types.ts +1 -2
- package/src/core/exec.ts +16 -100
- package/src/core/extensions/index.ts +1 -7
- package/src/core/extensions/loader.ts +1 -1
- package/src/core/extensions/runner.ts +1 -1
- package/src/core/extensions/types.ts +2 -2
- package/src/core/extensions/wrapper.ts +15 -20
- package/src/core/frontmatter.ts +1 -1
- package/src/core/history-storage.ts +3 -6
- package/src/core/hooks/index.ts +2 -2
- package/src/core/hooks/loader.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +14 -26
- package/src/core/hooks/types.ts +1 -2
- package/src/core/keybindings.ts +1 -1
- package/src/core/mcp/client.ts +13 -13
- package/src/core/mcp/json-rpc.ts +1 -1
- package/src/core/mcp/loader.ts +1 -1
- package/src/core/mcp/manager.ts +2 -2
- package/src/core/mcp/tool-cache.ts +1 -1
- package/src/core/mcp/transports/http.ts +32 -70
- package/src/core/model-registry.ts +1 -1
- package/src/core/plugins/installer.ts +13 -11
- package/src/core/prompt-templates.ts +4 -9
- package/src/core/python-executor.ts +23 -18
- package/src/core/python-gateway-coordinator.ts +29 -28
- package/src/core/python-kernel.ts +230 -211
- package/src/core/sdk.ts +10 -13
- package/src/core/session-manager.ts +1 -1
- package/src/core/settings-manager.ts +22 -9
- package/src/core/skills.ts +1 -1
- package/src/core/ssh/connection-manager.ts +19 -33
- package/src/core/ssh/ssh-executor.ts +39 -35
- package/src/core/ssh/sshfs-mount.ts +14 -33
- package/src/core/storage-migration.ts +1 -1
- package/src/core/streaming-output.ts +183 -127
- package/src/core/system-prompt.ts +119 -79
- package/src/core/title-generator.ts +1 -1
- package/src/core/tools/ask.ts +2 -2
- package/src/core/tools/bash.ts +3 -3
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +1 -1
- package/src/core/tools/exa/render.ts +1 -1
- package/src/core/tools/find.ts +39 -71
- package/src/core/tools/gemini-image.ts +1 -1
- package/src/core/tools/grep.ts +88 -100
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/ls.ts +1 -1
- package/src/core/tools/lsp/client.ts +50 -50
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +1 -1
- package/src/core/tools/lsp/config.ts +1 -1
- package/src/core/tools/lsp/index.ts +2 -4
- package/src/core/tools/lsp/lspmux.ts +1 -1
- package/src/core/tools/lsp/rust-analyzer.ts +2 -2
- package/src/core/tools/lsp/utils.ts +0 -14
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/patch/shared.ts +3 -4
- package/src/core/tools/python.ts +3 -3
- package/src/core/tools/read.ts +29 -68
- package/src/core/tools/render-utils.ts +0 -5
- package/src/core/tools/ssh.ts +3 -3
- package/src/core/tools/task/model-resolver.ts +7 -9
- package/src/core/tools/task/worker.ts +144 -139
- package/src/core/tools/todo-write.ts +1 -1
- package/src/core/tools/truncate.ts +2 -2
- package/src/core/tools/web-fetch.ts +13 -15
- package/src/core/tools/web-scrapers/types.ts +1 -3
- package/src/core/tools/web-scrapers/utils.ts +14 -13
- package/src/core/tools/web-scrapers/youtube.ts +39 -12
- package/src/core/tools/web-search/auth.ts +1 -1
- package/src/core/tools/write.ts +1 -1
- package/src/core/ttsr.ts +1 -1
- package/src/core/utils.ts +1 -187
- package/src/core/voice-controller.ts +1 -1
- package/src/core/voice-supervisor.ts +11 -38
- package/src/core/voice.ts +1 -8
- package/src/discovery/codex.ts +1 -1
- package/src/index.ts +4 -4
- package/src/main.ts +5 -10
- package/src/migrations.ts +1 -1
- package/src/modes/index.ts +7 -40
- package/src/modes/interactive/components/extensions/state-manager.ts +1 -1
- package/src/modes/interactive/components/hook-editor.ts +12 -9
- package/src/modes/interactive/components/login-dialog.ts +24 -11
- package/src/modes/interactive/components/settings-defs.ts +9 -0
- package/src/modes/interactive/components/status-line.ts +36 -35
- package/src/modes/interactive/components/todo-display.ts +1 -1
- package/src/modes/interactive/components/tool-execution.ts +1 -1
- package/src/modes/interactive/controllers/command-controller.ts +50 -84
- package/src/modes/interactive/controllers/extension-ui-controller.ts +76 -76
- package/src/modes/interactive/controllers/input-controller.ts +12 -11
- package/src/modes/interactive/interactive-mode.ts +10 -11
- package/src/modes/interactive/theme/theme.ts +1 -1
- package/src/modes/interactive/types.ts +1 -1
- package/src/modes/rpc/rpc-client.ts +91 -121
- package/src/modes/rpc/rpc-mode.ts +71 -79
- package/src/prompts/system/ttsr-interrupt.md +7 -0
- package/src/utils/clipboard.ts +57 -141
- package/src/utils/shell-snapshot.ts +12 -60
- package/src/utils/shell.ts +35 -56
- package/src/utils/tools-manager.ts +42 -71
- package/src/core/logger.ts +0 -111
- package/src/modes/cleanup.ts +0 -23
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
1
2
|
import { tmpdir } from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
5
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
6
7
|
import { type Static, Type } from "@sinclair/typebox";
|
|
8
|
+
import { $ } from "bun";
|
|
7
9
|
import { nanoid } from "nanoid";
|
|
8
10
|
import { parse as parseHtml } from "node-html-parser";
|
|
9
11
|
import { type Theme, theme } from "../../modes/interactive/theme/theme";
|
|
@@ -69,25 +71,21 @@ const CONVERTIBLE_EXTENSIONS = new Set([
|
|
|
69
71
|
// Utilities
|
|
70
72
|
// =============================================================================
|
|
71
73
|
|
|
72
|
-
type SpawnSyncOptions = NonNullable<Parameters<typeof Bun.spawnSync>[1]>;
|
|
73
|
-
|
|
74
74
|
/**
|
|
75
75
|
* Execute a command and return stdout
|
|
76
76
|
*/
|
|
77
|
-
|
|
77
|
+
|
|
78
|
+
async function exec(
|
|
78
79
|
cmd: string,
|
|
79
80
|
args: string[],
|
|
80
81
|
options?: { timeout?: number; input?: string | Buffer },
|
|
81
|
-
): { stdout: string; stderr: string; ok: boolean } {
|
|
82
|
-
|
|
83
|
-
const result =
|
|
84
|
-
|
|
85
|
-
stdout: "pipe",
|
|
86
|
-
stderr: "pipe",
|
|
87
|
-
});
|
|
82
|
+
): Promise<{ stdout: string; stderr: string; ok: boolean }> {
|
|
83
|
+
void options;
|
|
84
|
+
const result = await $`${cmd} ${args}`.quiet().nothrow();
|
|
85
|
+
const decoder = new TextDecoder();
|
|
88
86
|
return {
|
|
89
|
-
stdout: result.stdout
|
|
90
|
-
stderr: result.stderr
|
|
87
|
+
stdout: result.stdout ? decoder.decode(result.stdout) : "",
|
|
88
|
+
stderr: result.stderr ? decoder.decode(result.stderr) : "",
|
|
91
89
|
ok: result.exitCode === 0,
|
|
92
90
|
};
|
|
93
91
|
}
|
|
@@ -420,7 +418,7 @@ async function renderHtmlToText(
|
|
|
420
418
|
if (lynx) {
|
|
421
419
|
const normalizedPath = tmpFile.replace(/\\/g, "/");
|
|
422
420
|
const fileUrl = normalizedPath.startsWith("/") ? `file://${normalizedPath}` : `file:///${normalizedPath}`;
|
|
423
|
-
const result = exec("lynx", ["-dump", "-nolist", "-width", "120", fileUrl], { timeout });
|
|
421
|
+
const result = await exec("lynx", ["-dump", "-nolist", "-width", "120", fileUrl], { timeout });
|
|
424
422
|
if (result.ok) {
|
|
425
423
|
return { content: result.stdout, ok: true, method: "lynx" };
|
|
426
424
|
}
|
|
@@ -429,7 +427,7 @@ async function renderHtmlToText(
|
|
|
429
427
|
// Fall back to html2text (auto-install via uv/pip)
|
|
430
428
|
const html2text = await ensureTool("html2text", true);
|
|
431
429
|
if (html2text) {
|
|
432
|
-
const result = exec(html2text, [tmpFile], { timeout });
|
|
430
|
+
const result = await exec(html2text, [tmpFile], { timeout });
|
|
433
431
|
if (result.ok) {
|
|
434
432
|
return { content: result.stdout, ok: true, method: "html2text" };
|
|
435
433
|
}
|
|
@@ -438,7 +436,7 @@ async function renderHtmlToText(
|
|
|
438
436
|
return { content: "", ok: false, method: "none" };
|
|
439
437
|
} finally {
|
|
440
438
|
try {
|
|
441
|
-
await
|
|
439
|
+
await rm(tmpFile, { force: true });
|
|
442
440
|
} catch {}
|
|
443
441
|
}
|
|
444
442
|
}
|
|
@@ -158,9 +158,7 @@ export async function loadPage(url: string, options: LoadPageOptions = {}): Prom
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
const
|
|
162
|
-
const content = decoder.decode(Buffer.concat(chunks));
|
|
163
|
-
|
|
161
|
+
const content = Buffer.concat(chunks).toString("utf-8");
|
|
164
162
|
if (isBotBlocked(response.status, content) && attempt < USER_AGENTS.length - 1) {
|
|
165
163
|
continue;
|
|
166
164
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
1
2
|
import { tmpdir } from "node:os";
|
|
2
3
|
import * as path from "node:path";
|
|
4
|
+
import { $ } from "bun";
|
|
3
5
|
import { nanoid } from "nanoid";
|
|
4
6
|
import { ensureTool } from "../../../utils/tools-manager";
|
|
5
7
|
import { createRequestSignal } from "./types";
|
|
@@ -13,18 +15,17 @@ interface ExecResult {
|
|
|
13
15
|
exitCode: number;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
});
|
|
18
|
+
async function exec(
|
|
19
|
+
cmd: string,
|
|
20
|
+
args: string[],
|
|
21
|
+
options?: { timeout?: number; input?: string | Buffer },
|
|
22
|
+
): Promise<ExecResult> {
|
|
23
|
+
void options;
|
|
24
|
+
const result = await $`${cmd} ${args}`.quiet().nothrow();
|
|
25
|
+
const decoder = new TextDecoder();
|
|
25
26
|
return {
|
|
26
|
-
stdout: result.stdout
|
|
27
|
-
stderr: result.stderr
|
|
27
|
+
stdout: result.stdout ? decoder.decode(result.stdout) : "",
|
|
28
|
+
stderr: result.stderr ? decoder.decode(result.stderr) : "",
|
|
28
29
|
ok: result.exitCode === 0,
|
|
29
30
|
exitCode: result.exitCode ?? -1,
|
|
30
31
|
};
|
|
@@ -71,7 +72,7 @@ export async function convertWithMarkitdown(
|
|
|
71
72
|
|
|
72
73
|
try {
|
|
73
74
|
await Bun.write(tmpFile, content);
|
|
74
|
-
const result = exec(markitdown, [tmpFile], { timeout });
|
|
75
|
+
const result = await exec(markitdown, [tmpFile], { timeout });
|
|
75
76
|
if (!result.ok) {
|
|
76
77
|
const stderr = result.stderr.trim();
|
|
77
78
|
return {
|
|
@@ -83,7 +84,7 @@ export async function convertWithMarkitdown(
|
|
|
83
84
|
return { content: result.stdout, ok: true };
|
|
84
85
|
} finally {
|
|
85
86
|
try {
|
|
86
|
-
await
|
|
87
|
+
await rm(tmpFile, { force: true });
|
|
87
88
|
} catch {}
|
|
88
89
|
}
|
|
89
90
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { unlinkSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { cspawn } from "@oh-my-pi/pi-utils";
|
|
4
5
|
import type { FileSink } from "bun";
|
|
5
6
|
import { nanoid } from "nanoid";
|
|
6
7
|
import { ensureTool } from "../../../utils/tools-manager";
|
|
@@ -15,12 +16,21 @@ async function exec(
|
|
|
15
16
|
args: string[],
|
|
16
17
|
options?: { timeout?: number; input?: string | Buffer; signal?: AbortSignal },
|
|
17
18
|
): Promise<{ stdout: string; stderr: string; ok: boolean; exitCode: number | null }> {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
const onAbort = () => controller.abort(options?.signal?.reason ?? new Error("Aborted"));
|
|
21
|
+
if (options?.signal) {
|
|
22
|
+
if (options.signal.aborted) {
|
|
23
|
+
onAbort();
|
|
24
|
+
} else {
|
|
25
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const timeoutId =
|
|
29
|
+
options?.timeout && options.timeout > 0
|
|
30
|
+
? setTimeout(() => controller.abort(new Error("Timeout")), options.timeout)
|
|
31
|
+
: undefined;
|
|
32
|
+
const proc = cspawn([cmd, ...args], {
|
|
33
|
+
signal: controller.signal,
|
|
24
34
|
});
|
|
25
35
|
|
|
26
36
|
if (options?.input && proc.stdin) {
|
|
@@ -37,17 +47,34 @@ async function exec(
|
|
|
37
47
|
}
|
|
38
48
|
}
|
|
39
49
|
|
|
40
|
-
const [stdout, stderr] = await Promise.all([
|
|
41
|
-
(proc.stdout
|
|
42
|
-
(proc.stderr
|
|
50
|
+
const [stdout, stderr, exitResult] = await Promise.all([
|
|
51
|
+
new Response(proc.stdout).text(),
|
|
52
|
+
new Response(proc.stderr).text(),
|
|
53
|
+
(async () => {
|
|
54
|
+
try {
|
|
55
|
+
await proc.exited;
|
|
56
|
+
return proc.exitCode ?? 0;
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err && typeof err === "object" && "exitCode" in err) {
|
|
59
|
+
const exitValue = (err as { exitCode?: number }).exitCode;
|
|
60
|
+
if (typeof exitValue === "number") {
|
|
61
|
+
return exitValue;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
65
|
+
}
|
|
66
|
+
})(),
|
|
43
67
|
]);
|
|
44
|
-
|
|
68
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
69
|
+
if (options?.signal) {
|
|
70
|
+
options.signal.removeEventListener("abort", onAbort);
|
|
71
|
+
}
|
|
45
72
|
|
|
46
73
|
return {
|
|
47
74
|
stdout,
|
|
48
75
|
stderr,
|
|
49
|
-
ok:
|
|
50
|
-
exitCode,
|
|
76
|
+
ok: exitResult === 0,
|
|
77
|
+
exitCode: exitResult,
|
|
51
78
|
};
|
|
52
79
|
}
|
|
53
80
|
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { buildAnthropicHeaders as buildProviderAnthropicHeaders } from "@oh-my-pi/pi-ai";
|
|
14
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
14
15
|
import { getAgentDbPath, getConfigDirPaths } from "../../../config";
|
|
15
16
|
import { AgentStorage } from "../../agent-storage";
|
|
16
17
|
import type { AuthCredential, AuthCredentialEntry, AuthStorageData } from "../../auth-storage";
|
|
17
|
-
import { logger } from "../../logger";
|
|
18
18
|
import { migrateJsonStorage } from "../../storage-migration";
|
|
19
19
|
import type { AnthropicAuthConfig, AnthropicOAuthCredential, ModelsJson } from "./types";
|
|
20
20
|
|
package/src/core/tools/write.ts
CHANGED
|
@@ -7,13 +7,13 @@ import type {
|
|
|
7
7
|
} from "@oh-my-pi/pi-agent-core";
|
|
8
8
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
10
|
+
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
11
|
import { Type } from "@sinclair/typebox";
|
|
11
12
|
import { getLanguageFromPath, highlightCode, type Theme } from "../../modes/interactive/theme/theme";
|
|
12
13
|
import writeDescription from "../../prompts/tools/write.md" with { type: "text" };
|
|
13
14
|
import type { RenderResultOptions } from "../custom-tools/types";
|
|
14
15
|
import { renderPromptTemplate } from "../prompt-templates";
|
|
15
16
|
import type { ToolSession } from "../sdk";
|
|
16
|
-
import { untilAborted } from "../utils";
|
|
17
17
|
import {
|
|
18
18
|
createLspWritethrough,
|
|
19
19
|
type FileDiagnosticsResult,
|
package/src/core/ttsr.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* injected as a system reminder, and the request is retried.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
9
10
|
import type { Rule } from "../capability/rule";
|
|
10
|
-
import { logger } from "./logger";
|
|
11
11
|
import type { TtsrSettings } from "./settings-manager";
|
|
12
12
|
|
|
13
13
|
interface TtsrEntry {
|
package/src/core/utils.ts
CHANGED
|
@@ -1,187 +1 @@
|
|
|
1
|
-
|
|
2
|
-
const kAbortError = new Error("Operation aborted");
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Runs a promise-returning function (`pr`). If the given AbortSignal is aborted before or during
|
|
6
|
-
* execution, the promise is rejected with a standard error.
|
|
7
|
-
*
|
|
8
|
-
* @param signal - Optional AbortSignal to cancel the operation
|
|
9
|
-
* @param pr - Function returning a promise to run
|
|
10
|
-
* @returns Promise resolving as `pr` would, or rejecting on abort
|
|
11
|
-
*/
|
|
12
|
-
export function untilAborted<T>(signal: AbortSignal | undefined | null, pr: () => Promise<T>): Promise<T> {
|
|
13
|
-
if (!signal) {
|
|
14
|
-
return pr();
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (signal.aborted) {
|
|
18
|
-
return Promise.reject(kAbortError);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
return new Promise((resolve, reject) => {
|
|
22
|
-
const listener = () => reject(kAbortError);
|
|
23
|
-
signal.addEventListener("abort", listener, { once: true });
|
|
24
|
-
|
|
25
|
-
signal.throwIfAborted();
|
|
26
|
-
|
|
27
|
-
pr()
|
|
28
|
-
.then(resolve, reject)
|
|
29
|
-
.finally(() => {
|
|
30
|
-
signal.removeEventListener("abort", listener);
|
|
31
|
-
});
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Memoizes a function with no arguments, calling it once and caching the result.
|
|
37
|
-
*
|
|
38
|
-
* @param fn - Function to be called once
|
|
39
|
-
* @returns A function that returns the cached result of `fn`
|
|
40
|
-
*/
|
|
41
|
-
export function once<T>(fn: () => T): () => T {
|
|
42
|
-
let store = undefined as { value: T } | undefined;
|
|
43
|
-
return () => {
|
|
44
|
-
if (store) {
|
|
45
|
-
return store.value;
|
|
46
|
-
}
|
|
47
|
-
const value = fn();
|
|
48
|
-
store = { value };
|
|
49
|
-
return value;
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// ScopeSignal is a cancellation/helper utility similar to AbortController but
|
|
54
|
-
// allows composition of an existing AbortSignal and/or a timeout. It exposes a
|
|
55
|
-
// simple API for cancellation observation (finally, catch).
|
|
56
|
-
interface ScopeSignalOptions {
|
|
57
|
-
signal?: AbortSignal;
|
|
58
|
-
timeout?: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const kTimeoutReason = new Error("Timeout");
|
|
62
|
-
const kDisposedReason = new Error("Disposed");
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Type of signal exit (None = disposed, TimedOut = timed out, Aborted = underlying signal aborted)
|
|
66
|
-
*/
|
|
67
|
-
enum ExitReason {
|
|
68
|
-
None = 0,
|
|
69
|
-
TimedOut = 1,
|
|
70
|
-
Aborted = 2,
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* ScopeSignal: composable cancellation for async work–observes an external AbortSignal and/or a timeout.
|
|
75
|
-
*
|
|
76
|
-
* Use .finally(fn) to register a one-time callback invoked on *any* exit (abort, timeout, or manual dispose).
|
|
77
|
-
* Use .catch(fn) to register a one-time callback invoked only on abort/timeout.
|
|
78
|
-
*
|
|
79
|
-
* Disposing ScopeSignal disables further callbacks.
|
|
80
|
-
*/
|
|
81
|
-
export class ScopeSignal implements Disposable {
|
|
82
|
-
#signal: AbortSignal | undefined;
|
|
83
|
-
#timer: NodeJS.Timeout | undefined;
|
|
84
|
-
#exit = undefined as ExitReason | undefined;
|
|
85
|
-
#onAbort: (() => void) | undefined;
|
|
86
|
-
#callbacks?: (() => void)[];
|
|
87
|
-
#reason: unknown | undefined;
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Provides abort/timeout reason (Error or user-defined).
|
|
91
|
-
*/
|
|
92
|
-
get reason(): unknown | undefined {
|
|
93
|
-
return this.#reason;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* True if exited due to external AbortSignal or timeout.
|
|
98
|
-
*/
|
|
99
|
-
get aborted(): boolean {
|
|
100
|
-
return this.#exit !== undefined && this.#exit > ExitReason.None;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* True if this ScopeSignal timed out (not external abort).
|
|
105
|
-
*/
|
|
106
|
-
timedOut(): boolean {
|
|
107
|
-
return this.#exit === ExitReason.TimedOut;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Create a new ScopeSignal, optionally observing an AbortSignal and/or auto-aborting after a timeout (ms).
|
|
112
|
-
*/
|
|
113
|
-
constructor(options?: ScopeSignalOptions) {
|
|
114
|
-
const { signal, timeout } = options ?? {};
|
|
115
|
-
|
|
116
|
-
if (signal?.aborted) {
|
|
117
|
-
this.#abort(ExitReason.Aborted, signal.reason); // Immediately abort if already-aborted
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
if (timeout && timeout <= 0) {
|
|
121
|
-
this.#abort(ExitReason.TimedOut, kTimeoutReason);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Observe external signal if provided
|
|
126
|
-
if (signal) {
|
|
127
|
-
const onAbort = () => {
|
|
128
|
-
this.#abort(ExitReason.Aborted, signal.reason);
|
|
129
|
-
};
|
|
130
|
-
this.#signal = signal;
|
|
131
|
-
this.#onAbort = onAbort;
|
|
132
|
-
this.#signal.addEventListener("abort", onAbort, { once: true });
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Set up timeout if provided
|
|
136
|
-
if (timeout) {
|
|
137
|
-
this.#timer = setTimeout(() => {
|
|
138
|
-
this.#abort(ExitReason.TimedOut, kTimeoutReason);
|
|
139
|
-
}, timeout);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Register a one-time callback invoked on any exit (abort, timeout, or manual dispose).
|
|
145
|
-
* Runs immediately if already exited.
|
|
146
|
-
*/
|
|
147
|
-
finally(onfinally: () => void): void {
|
|
148
|
-
if (this.#exit !== undefined) {
|
|
149
|
-
onfinally();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
this.#callbacks ??= [];
|
|
153
|
-
this.#callbacks.push(onfinally);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Register a one-time callback invoked only if exited due to abort/timeout (not normal disposal).
|
|
158
|
-
*/
|
|
159
|
-
catch(oncatch: (reason: unknown) => void): void {
|
|
160
|
-
this.finally(() => {
|
|
161
|
-
if (this.aborted) {
|
|
162
|
-
oncatch(this.reason);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** Internal: cause exit; only first call takes effect. */
|
|
168
|
-
#abort(exit: ExitReason, reason?: unknown): void {
|
|
169
|
-
if (this.#exit !== undefined) return;
|
|
170
|
-
this.#reason = reason;
|
|
171
|
-
clearTimeout(this.#timer);
|
|
172
|
-
this.#signal?.removeEventListener("abort", this.#onAbort!);
|
|
173
|
-
|
|
174
|
-
this.#exit = exit;
|
|
175
|
-
|
|
176
|
-
const callbacks = this.#callbacks;
|
|
177
|
-
this.#callbacks = undefined;
|
|
178
|
-
callbacks?.forEach((fn) => void fn());
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Dispose: marks as normally exited (not abort/timeout); disables further callback registration.
|
|
183
|
-
*/
|
|
184
|
-
[Symbol.dispose](): void {
|
|
185
|
-
this.#abort(ExitReason.None, kDisposedReason);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
1
|
+
export { abortableSleep, once, untilAborted } from "@oh-my-pi/pi-utils";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import { Agent, run, setDefaultOpenAIKey } from "@openai/agents";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { logger } from "./logger";
|
|
4
4
|
import type { ModelRegistry } from "./model-registry";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_CONTROLLER_MODEL = process.env.OMP_VOICE_CONTROLLER_MODEL ?? "gpt-4o-mini";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger, ptree } from "@oh-my-pi/pi-utils";
|
|
1
2
|
import {
|
|
2
3
|
RealtimeAgent,
|
|
3
4
|
RealtimeSession,
|
|
@@ -6,10 +7,8 @@ import {
|
|
|
6
7
|
type TransportLayerAudio,
|
|
7
8
|
tool,
|
|
8
9
|
} from "@openai/agents/realtime";
|
|
9
|
-
import type { Subprocess } from "bun";
|
|
10
10
|
import type { ReadableStreamDefaultReader as WebReadableStreamDefaultReader } from "stream/web";
|
|
11
11
|
import { z } from "zod";
|
|
12
|
-
import { logger } from "./logger";
|
|
13
12
|
import type { ModelRegistry } from "./model-registry";
|
|
14
13
|
|
|
15
14
|
const DEFAULT_REALTIME_MODEL = process.env.OMP_VOICE_REALTIME_MODEL ?? "gpt-realtime";
|
|
@@ -372,9 +371,9 @@ function rms16le(buffer: Uint8Array): number {
|
|
|
372
371
|
|
|
373
372
|
export class VoiceSupervisor {
|
|
374
373
|
private session: RealtimeSession | undefined = undefined;
|
|
375
|
-
private captureProcess:
|
|
374
|
+
private captureProcess: ptree.ChildProcess | undefined = undefined;
|
|
376
375
|
private captureReader: WebReadableStreamDefaultReader<Uint8Array> | undefined = undefined;
|
|
377
|
-
private playbackProcess:
|
|
376
|
+
private playbackProcess: ptree.ChildProcess | undefined = undefined;
|
|
378
377
|
private playbackWriter:
|
|
379
378
|
| {
|
|
380
379
|
write: (chunk: Uint8Array) => Promise<void>;
|
|
@@ -744,15 +743,11 @@ export class VoiceSupervisor {
|
|
|
744
743
|
|
|
745
744
|
const { command, env: captureEnv } = captureResult;
|
|
746
745
|
logger.debug("voice-supervisor: starting mic capture", { command, env: captureEnv });
|
|
747
|
-
const proc =
|
|
748
|
-
stdin: "ignore",
|
|
749
|
-
stdout: "pipe",
|
|
750
|
-
stderr: "pipe",
|
|
746
|
+
const proc = ptree.cspawn(command, {
|
|
751
747
|
env: captureEnv ? { ...process.env, ...captureEnv } : undefined,
|
|
752
748
|
});
|
|
753
749
|
this.captureProcess = proc;
|
|
754
|
-
|
|
755
|
-
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
750
|
+
const reader = proc.stdout.getReader();
|
|
756
751
|
this.captureReader = reader;
|
|
757
752
|
|
|
758
753
|
(async () => {
|
|
@@ -812,7 +807,7 @@ export class VoiceSupervisor {
|
|
|
812
807
|
}
|
|
813
808
|
if (this.captureProcess) {
|
|
814
809
|
try {
|
|
815
|
-
this.captureProcess.kill();
|
|
810
|
+
this.captureProcess.kill("SIGINT");
|
|
816
811
|
} catch {
|
|
817
812
|
// ignore
|
|
818
813
|
}
|
|
@@ -829,14 +824,10 @@ export class VoiceSupervisor {
|
|
|
829
824
|
}
|
|
830
825
|
|
|
831
826
|
logger.debug("voice-supervisor: starting audio playback", { command });
|
|
832
|
-
const proc =
|
|
827
|
+
const proc = ptree.cspawn(command, {
|
|
833
828
|
stdin: "pipe",
|
|
834
|
-
stdout: "ignore",
|
|
835
|
-
stderr: "pipe",
|
|
836
829
|
});
|
|
837
830
|
const startedAt = Date.now();
|
|
838
|
-
const stderrBuffer = { text: "" };
|
|
839
|
-
this.readStderr(proc.stderr, stderrBuffer);
|
|
840
831
|
|
|
841
832
|
this.playbackProcess = proc;
|
|
842
833
|
const stdin = proc.stdin;
|
|
@@ -876,17 +867,18 @@ export class VoiceSupervisor {
|
|
|
876
867
|
}
|
|
877
868
|
|
|
878
869
|
proc.exited
|
|
879
|
-
.then((
|
|
870
|
+
.then(() => {
|
|
871
|
+
const code = proc.exitCode;
|
|
880
872
|
if (this.playbackProcess === proc) {
|
|
881
873
|
this.playbackProcess = undefined;
|
|
882
874
|
this.playbackWriter = undefined;
|
|
883
875
|
}
|
|
884
|
-
const trimmed =
|
|
876
|
+
const trimmed = proc.peekStderr().trim();
|
|
885
877
|
if (trimmed) {
|
|
886
878
|
logger.debug("voice-supervisor: playback stderr", { stderr: trimmed });
|
|
887
879
|
}
|
|
888
880
|
const elapsed = Date.now() - startedAt;
|
|
889
|
-
if (code !== 0 && elapsed < 2000 && this.active) {
|
|
881
|
+
if (code !== 0 && elapsed < 2000 && this.active && code !== null) {
|
|
890
882
|
this.maybeWarnPlaybackFailure(trimmed || `exit code ${code}`);
|
|
891
883
|
}
|
|
892
884
|
})
|
|
@@ -915,25 +907,6 @@ export class VoiceSupervisor {
|
|
|
915
907
|
this.playbackWriter = undefined;
|
|
916
908
|
}
|
|
917
909
|
|
|
918
|
-
private readStderr(stderr: Subprocess["stderr"], buffer: { text: string }): void {
|
|
919
|
-
if (!stderr || typeof stderr === "number") return;
|
|
920
|
-
const reader = (stderr as ReadableStream<Uint8Array>).getReader();
|
|
921
|
-
const decoder = new TextDecoder();
|
|
922
|
-
(async () => {
|
|
923
|
-
while (true) {
|
|
924
|
-
const { value, done } = await reader.read();
|
|
925
|
-
if (done || !value) break;
|
|
926
|
-
buffer.text += decoder.decode(value, { stream: true });
|
|
927
|
-
if (buffer.text.length > 4000) {
|
|
928
|
-
buffer.text = buffer.text.slice(0, 4000);
|
|
929
|
-
break;
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
})().catch(() => {
|
|
933
|
-
// ignore
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
|
|
937
910
|
private maybeWarnPlaybackFailure(message: string): void {
|
|
938
911
|
if (!this.callbacks.onWarning) return;
|
|
939
912
|
const now = Date.now();
|
package/src/core/voice.ts
CHANGED
|
@@ -2,9 +2,9 @@ import { unlinkSync } from "node:fs";
|
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { nanoid } from "nanoid";
|
|
6
7
|
import voiceSummaryPrompt from "../prompts/voice-summary.md" with { type: "text" };
|
|
7
|
-
import { logger } from "./logger";
|
|
8
8
|
import type { ModelRegistry } from "./model-registry";
|
|
9
9
|
import { findSmolModel } from "./model-resolver";
|
|
10
10
|
import { renderPromptTemplate } from "./prompt-templates";
|
|
@@ -143,13 +143,6 @@ function buildRecordingCommand(filePath: string, sampleRate: number, channels: n
|
|
|
143
143
|
return null;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
/**
|
|
147
|
-
* @deprecated Use `new VoiceRecording(settings)` instead.
|
|
148
|
-
*/
|
|
149
|
-
export function startVoiceRecording(settings: VoiceSettings): VoiceRecordingHandle {
|
|
150
|
-
return new VoiceRecording(settings);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
146
|
export async function transcribeAudio(
|
|
154
147
|
filePath: string,
|
|
155
148
|
apiKey: string,
|
package/src/discovery/codex.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import { parse as parseToml } from "smol-toml";
|
|
12
13
|
import type { ContextFile } from "../capability/context-file";
|
|
13
14
|
import { contextFileCapability } from "../capability/context-file";
|
|
@@ -30,7 +31,6 @@ import type { CustomTool } from "../capability/tool";
|
|
|
30
31
|
import { toolCapability } from "../capability/tool";
|
|
31
32
|
import type { LoadContext, LoadResult } from "../capability/types";
|
|
32
33
|
import { parseFrontmatter } from "../core/frontmatter";
|
|
33
|
-
import { logger } from "../core/logger";
|
|
34
34
|
import {
|
|
35
35
|
createSourceMeta,
|
|
36
36
|
discoverExtensionModulePaths,
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
export { StringEnum } from "@oh-my-pi/pi-ai";
|
|
6
6
|
// Re-export TUI components for custom tool rendering
|
|
7
7
|
export { Container, Markdown, Spacer, Text } from "@oh-my-pi/pi-tui";
|
|
8
|
+
// Logging
|
|
9
|
+
export { logger } from "@oh-my-pi/pi-utils";
|
|
8
10
|
export { getAgentDir, VERSION } from "./config";
|
|
9
11
|
export {
|
|
10
12
|
AgentSession,
|
|
@@ -49,7 +51,6 @@ export type {
|
|
|
49
51
|
CustomCommandsLoadResult,
|
|
50
52
|
LoadedCustomCommand,
|
|
51
53
|
} from "./core/custom-commands/types";
|
|
52
|
-
// Custom tools
|
|
53
54
|
export type {
|
|
54
55
|
AgentToolUpdateCallback,
|
|
55
56
|
CustomTool,
|
|
@@ -63,8 +64,8 @@ export type {
|
|
|
63
64
|
LoadedCustomTool,
|
|
64
65
|
RenderResultOptions,
|
|
65
66
|
} from "./core/custom-tools/index";
|
|
67
|
+
// Custom tools
|
|
66
68
|
export { CustomToolLoader, discoverAndLoadCustomTools, loadCustomTools } from "./core/custom-tools/index";
|
|
67
|
-
// Extension types and utilities
|
|
68
69
|
export type {
|
|
69
70
|
AppAction,
|
|
70
71
|
Extension,
|
|
@@ -95,6 +96,7 @@ export type {
|
|
|
95
96
|
UserBashEvent,
|
|
96
97
|
UserBashEventResult,
|
|
97
98
|
} from "./core/extensions/index";
|
|
99
|
+
// Extension types and utilities
|
|
98
100
|
export {
|
|
99
101
|
discoverAndLoadExtensions,
|
|
100
102
|
ExtensionRunner,
|
|
@@ -110,8 +112,6 @@ export {
|
|
|
110
112
|
// Hook system types (legacy re-export)
|
|
111
113
|
export type * from "./core/hooks/index";
|
|
112
114
|
export { formatKeyHint, formatKeyHints } from "./core/keybindings";
|
|
113
|
-
// Logging
|
|
114
|
-
export { type Logger, logger } from "./core/logger";
|
|
115
115
|
export { convertToLlm } from "./core/messages";
|
|
116
116
|
export { ModelRegistry } from "./core/model-registry";
|
|
117
117
|
// Prompt templates
|