@oh-my-pi/pi-coding-agent 15.12.4 → 15.13.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 +304 -6
- package/dist/cli.js +1015 -881
- package/dist/types/async/job-manager.d.ts +15 -0
- package/dist/types/autolearn/controller.d.ts +25 -0
- package/dist/types/autolearn/managed-skills.d.ts +45 -0
- package/dist/types/autoresearch/state.d.ts +1 -1
- package/dist/types/autoresearch/types.d.ts +1 -1
- package/dist/types/cli/args.d.ts +19 -1
- package/dist/types/cli/session-picker.d.ts +1 -1
- package/dist/types/cli/setup-cli.d.ts +1 -1
- package/dist/types/cli/setup-model-picker.d.ts +14 -0
- package/dist/types/collab/protocol.d.ts +1 -1
- package/dist/types/commands/say.d.ts +24 -0
- package/dist/types/config/keybindings.d.ts +3 -3
- package/dist/types/config/model-registry.d.ts +10 -0
- package/dist/types/config/models-config-schema.d.ts +12 -0
- package/dist/types/config/models-config.d.ts +8 -2
- package/dist/types/config/settings-schema.d.ts +261 -58
- package/dist/types/export/html/index.d.ts +2 -1
- package/dist/types/extensibility/extensions/model-api.d.ts +17 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +47 -1
- package/dist/types/extensibility/hooks/index.d.ts +2 -1
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +9 -0
- package/dist/types/extensibility/plugins/loader.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +10 -0
- package/dist/types/goals/guided-setup.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/hindsight/transcript.d.ts +1 -1
- package/dist/types/index.d.ts +5 -0
- package/dist/types/internal-urls/local-protocol.d.ts +4 -2
- package/dist/types/main.d.ts +4 -3
- package/dist/types/mcp/startup-events.d.ts +11 -0
- package/dist/types/memories/index.d.ts +7 -0
- package/dist/types/memory-backend/local-backend.d.ts +4 -3
- package/dist/types/mnemopi/config.d.ts +4 -4
- package/dist/types/modes/components/agent-hub.d.ts +6 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -2
- package/dist/types/modes/components/compaction-summary-message.d.ts +15 -1
- package/dist/types/modes/components/custom-editor.d.ts +39 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/tool-execution.d.ts +26 -16
- package/dist/types/modes/components/transcript-container.d.ts +23 -2
- package/dist/types/modes/components/tree-selector.d.ts +1 -1
- package/dist/types/modes/components/usage-row.d.ts +3 -0
- package/dist/types/modes/controllers/command-controller.d.ts +2 -2
- package/dist/types/modes/controllers/input-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/gradient-highlight.d.ts +9 -4
- package/dist/types/modes/image-references.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +27 -3
- package/dist/types/modes/magic-keywords.d.ts +13 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +35 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +9 -1
- package/dist/types/modes/runtime-init.d.ts +4 -0
- package/dist/types/modes/theme/theme.d.ts +13 -2
- package/dist/types/modes/types.d.ts +8 -2
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -1
- package/dist/types/registry/agent-registry.d.ts +17 -0
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +14 -2
- package/dist/types/session/indexed-session-storage.d.ts +3 -4
- package/dist/types/session/session-context.d.ts +39 -0
- package/dist/types/session/session-entries.d.ts +159 -0
- package/dist/types/session/session-listing.d.ts +69 -0
- package/dist/types/session/session-loader.d.ts +16 -0
- package/dist/types/session/session-manager.d.ts +82 -474
- package/dist/types/session/session-migrations.d.ts +12 -0
- package/dist/types/session/session-paths.d.ts +25 -0
- package/dist/types/session/session-persistence.d.ts +8 -0
- package/dist/types/session/session-storage.d.ts +11 -12
- package/dist/types/session/snapcompact-inline.d.ts +12 -1
- package/dist/types/session/snapcompact-savings-journal.d.ts +46 -0
- package/dist/types/session/tool-choice-queue.d.ts +6 -6
- package/dist/types/stt/asr-client.d.ts +90 -0
- package/dist/types/stt/asr-protocol.d.ts +97 -0
- package/dist/types/stt/asr-worker.d.ts +2 -0
- package/dist/types/stt/downloader.d.ts +38 -0
- package/dist/types/stt/endpointer.d.ts +59 -0
- package/dist/types/stt/index.d.ts +5 -1
- package/dist/types/stt/models.d.ts +120 -0
- package/dist/types/stt/recorder.d.ts +17 -0
- package/dist/types/stt/stt-controller.d.ts +6 -0
- package/dist/types/stt/transcriber.d.ts +5 -7
- package/dist/types/stt/wav.d.ts +29 -0
- package/dist/types/system-prompt.d.ts +4 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/task/index.d.ts +9 -1
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/tools/bash.d.ts +2 -2
- package/dist/types/tools/eval-render.d.ts +1 -1
- package/dist/types/tools/index.d.ts +11 -1
- package/dist/types/tools/irc.d.ts +1 -0
- package/dist/types/tools/learn.d.ts +51 -0
- package/dist/types/tools/manage-skill.d.ts +40 -0
- package/dist/types/tools/plan-mode-guard.d.ts +10 -0
- package/dist/types/tools/renderers.d.ts +7 -11
- package/dist/types/tools/ssh.d.ts +1 -1
- package/dist/types/tools/todo.d.ts +1 -1
- package/dist/types/tools/tts.d.ts +25 -0
- package/dist/types/tools/write.d.ts +1 -1
- package/dist/types/tts/downloader.d.ts +20 -0
- package/dist/types/tts/index.d.ts +8 -0
- package/dist/types/tts/models.d.ts +82 -0
- package/dist/types/tts/player.d.ts +32 -0
- package/dist/types/tts/runtime.d.ts +6 -0
- package/dist/types/tts/streaming-player.d.ts +41 -0
- package/dist/types/tts/tts-client.d.ts +93 -0
- package/dist/types/tts/tts-protocol.d.ts +95 -0
- package/dist/types/tts/tts-worker.d.ts +2 -0
- package/dist/types/tts/vocalizer.d.ts +41 -0
- package/dist/types/tts/wav.d.ts +8 -0
- package/dist/types/utils/tool-choice.d.ts +8 -0
- package/dist/types/utils/tools-manager.d.ts +2 -1
- package/dist/types/utils/tools-manager.test.d.ts +1 -0
- package/dist/types/web/scrapers/github.d.ts +1 -1
- package/package.json +15 -14
- package/src/async/job-manager.ts +49 -0
- package/src/autolearn/controller.ts +139 -0
- package/src/autolearn/managed-skills.ts +257 -0
- package/src/autoresearch/state.ts +1 -1
- package/src/autoresearch/types.ts +1 -1
- package/src/cli/args.ts +56 -2
- package/src/cli/session-picker.ts +2 -1
- package/src/cli/setup-cli.ts +148 -47
- package/src/cli/setup-model-picker.ts +43 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +45 -13
- package/src/collab/host.ts +1 -1
- package/src/collab/protocol.ts +1 -1
- package/src/commands/say.ts +102 -0
- package/src/commands/setup.ts +1 -1
- package/src/commit/agentic/tools/analyze-file.ts +3 -0
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-discovery.ts +11 -5
- package/src/config/model-registry.ts +64 -9
- package/src/config/models-config-schema.ts +4 -1
- package/src/config/models-config.ts +2 -1
- package/src/config/settings-schema.ts +248 -32
- package/src/config/settings.ts +10 -0
- package/src/discovery/builtin.ts +23 -1
- package/src/discovery/claude-plugins.ts +44 -5
- package/src/discovery/helpers.ts +41 -1
- package/src/eval/__tests__/budget-bridge.test.ts +1 -1
- package/src/eval/js/shared/prelude.txt +69 -17
- package/src/export/html/index.ts +3 -6
- package/src/extensibility/extensions/model-api.ts +41 -0
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +52 -1
- package/src/extensibility/extensions/wrapper.ts +41 -5
- package/src/extensibility/hooks/index.ts +2 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +43 -13
- package/src/extensibility/plugins/loader.ts +30 -19
- package/src/extensibility/plugins/manager.ts +221 -90
- package/src/extensibility/shared-events.ts +1 -1
- package/src/extensibility/skills.ts +96 -15
- package/src/goals/guided-setup.ts +133 -0
- package/src/goals/state.ts +1 -1
- package/src/hindsight/transcript.ts +1 -1
- package/src/index.ts +5 -0
- package/src/internal-urls/docs-index.generated.ts +10 -10
- package/src/internal-urls/history-protocol.ts +1 -1
- package/src/internal-urls/local-protocol.ts +29 -7
- package/src/main.ts +27 -7
- package/src/mcp/startup-events.ts +21 -0
- package/src/mcp/transports/stdio.ts +2 -1
- package/src/memories/index.ts +146 -11
- package/src/memory-backend/local-backend.ts +11 -5
- package/src/mnemopi/backend.ts +1 -0
- package/src/mnemopi/config.ts +26 -10
- package/src/modes/acp/acp-agent.ts +3 -5
- package/src/modes/components/agent-hub.ts +49 -4
- package/src/modes/components/assistant-message.ts +4 -37
- package/src/modes/components/compaction-summary-message.ts +125 -26
- package/src/modes/components/custom-editor.test.ts +96 -0
- package/src/modes/components/custom-editor.ts +164 -8
- package/src/modes/components/session-selector.ts +1 -1
- package/src/modes/components/settings-defs.ts +7 -0
- package/src/modes/components/tool-execution.ts +82 -43
- package/src/modes/components/transcript-container.ts +70 -1
- package/src/modes/components/tree-selector.ts +1 -1
- package/src/modes/components/usage-row.ts +18 -0
- package/src/modes/components/user-message.ts +4 -2
- package/src/modes/controllers/command-controller.ts +14 -4
- package/src/modes/controllers/event-controller.ts +78 -11
- package/src/modes/controllers/extension-ui-controller.ts +6 -0
- package/src/modes/controllers/input-controller.ts +258 -27
- package/src/modes/controllers/selector-controller.ts +12 -2
- package/src/modes/gradient-highlight.ts +21 -9
- package/src/modes/image-references.ts +20 -0
- package/src/modes/interactive-mode.ts +286 -40
- package/src/modes/magic-keywords.ts +27 -5
- package/src/modes/rpc/rpc-mode.ts +146 -14
- package/src/modes/rpc/rpc-subagents.ts +2 -2
- package/src/modes/rpc/rpc-types.ts +8 -2
- package/src/modes/runtime-init.ts +28 -3
- package/src/modes/theme/theme.ts +98 -50
- package/src/modes/types.ts +6 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/modes/utils/ui-helpers.ts +34 -6
- package/src/priority.json +5 -1
- package/src/prompts/agents/task.md +1 -0
- package/src/prompts/goals/guided-goal-interview.md +8 -0
- package/src/prompts/goals/guided-goal-system.md +12 -0
- package/src/prompts/memories/read-path.md +6 -0
- package/src/prompts/system/autolearn-guidance-learn.md +1 -0
- package/src/prompts/system/autolearn-guidance.md +7 -0
- package/src/prompts/system/autolearn-nudge.md +3 -0
- package/src/prompts/system/eager-task.md +7 -0
- package/src/prompts/system/eager-todo.md +11 -6
- package/src/prompts/system/subagent-system-prompt.md +4 -0
- package/src/prompts/system/system-prompt.md +10 -5
- package/src/prompts/system/title-marker-instruction.md +1 -0
- package/src/prompts/system/title-system-marker.md +16 -0
- package/src/prompts/tools/job.md +1 -0
- package/src/prompts/tools/learn.md +7 -0
- package/src/prompts/tools/manage-skill.md +9 -0
- package/src/prompts/tools/task.md +3 -0
- package/src/registry/agent-registry.ts +30 -0
- package/src/sdk.ts +88 -24
- package/src/secrets/obfuscator.ts +1 -1
- package/src/session/agent-session.ts +209 -87
- package/src/session/history-storage.ts +2 -2
- package/src/session/indexed-session-storage.ts +7 -17
- package/src/session/session-context.ts +352 -0
- package/src/session/session-entries.ts +194 -0
- package/src/session/session-listing.ts +588 -0
- package/src/session/session-loader.ts +106 -0
- package/src/session/session-manager.ts +933 -3145
- package/src/session/session-migrations.ts +78 -0
- package/src/session/session-paths.ts +193 -0
- package/src/session/session-persistence.ts +131 -0
- package/src/session/session-storage.ts +91 -50
- package/src/session/snapcompact-inline.ts +21 -1
- package/src/session/snapcompact-savings-journal.ts +113 -0
- package/src/session/tool-choice-queue.ts +23 -11
- package/src/slash-commands/builtin-registry.ts +25 -3
- package/src/stt/asr-client.ts +520 -0
- package/src/stt/asr-protocol.ts +65 -0
- package/src/stt/asr-worker.ts +790 -0
- package/src/stt/downloader.ts +107 -47
- package/src/stt/endpointer.ts +259 -0
- package/src/stt/index.ts +5 -1
- package/src/stt/models.ts +150 -0
- package/src/stt/recorder.ts +247 -60
- package/src/stt/stt-controller.ts +201 -22
- package/src/stt/transcriber.ts +37 -68
- package/src/stt/wav.ts +173 -0
- package/src/system-prompt.ts +8 -0
- package/src/task/agents.ts +1 -2
- package/src/task/executor.ts +49 -15
- package/src/task/index.ts +60 -6
- package/src/task/render.ts +83 -8
- package/src/task/types.ts +53 -0
- package/src/tools/ask.ts +8 -0
- package/src/tools/bash.ts +4 -3
- package/src/tools/eval-render.ts +4 -3
- package/src/tools/index.ts +40 -4
- package/src/tools/irc.ts +10 -2
- package/src/tools/job.ts +14 -2
- package/src/tools/learn.ts +144 -0
- package/src/tools/manage-skill.ts +104 -0
- package/src/tools/plan-mode-guard.ts +53 -19
- package/src/tools/renderers.ts +7 -11
- package/src/tools/ssh.ts +4 -3
- package/src/tools/todo.ts +1 -1
- package/src/tools/tts.ts +203 -92
- package/src/tools/write.ts +18 -2
- package/src/tts/downloader.ts +64 -0
- package/src/tts/index.ts +8 -0
- package/src/tts/models.ts +137 -0
- package/src/tts/player.ts +137 -0
- package/src/tts/runtime.ts +21 -0
- package/src/tts/streaming-player.ts +266 -0
- package/src/tts/tts-client.ts +647 -0
- package/src/tts/tts-protocol.ts +60 -0
- package/src/tts/tts-worker.ts +497 -0
- package/src/tts/vocalizer.ts +162 -0
- package/src/tts/wav.ts +58 -0
- package/src/utils/title-generator.ts +48 -5
- package/src/utils/tool-choice.ts +16 -0
- package/src/utils/tools-manager.test.ts +25 -0
- package/src/utils/tools-manager.ts +19 -1
- package/src/web/scrapers/github.ts +96 -0
- package/src/web/search/index.ts +13 -0
- package/src/web/search/providers/searxng.ts +13 -1
- package/dist/types/stt/setup.d.ts +0 -18
- package/src/stt/setup.ts +0 -52
- package/src/stt/transcribe.py +0 -70
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed-skills primitives for the experimental auto-learn feature.
|
|
3
|
+
*
|
|
4
|
+
* Managed skills are auto-generated/enhanced `SKILL.md` files kept in an
|
|
5
|
+
* isolated directory (`~/.omp/agent/managed-skills`) separate from
|
|
6
|
+
* user-authored skills (`~/.omp/agent/skills`). They are discovered and
|
|
7
|
+
* surfaced like normal skills, but every write here is confined to
|
|
8
|
+
* `getManagedSkillsDir()` — auto-management can never touch authored skills.
|
|
9
|
+
*/
|
|
10
|
+
import { constants as fsConstants, type Stats } from "node:fs";
|
|
11
|
+
import * as fs from "node:fs/promises";
|
|
12
|
+
import * as os from "node:os";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
15
|
+
import { YAML } from "bun";
|
|
16
|
+
import { SOURCE_PATHS } from "../discovery/helpers";
|
|
17
|
+
|
|
18
|
+
/** Provider id stamped on discovered managed skills (distinguishes them from authored). */
|
|
19
|
+
export const MANAGED_SKILLS_PROVIDER_ID = "omp-managed";
|
|
20
|
+
|
|
21
|
+
/** Hard cap on a managed SKILL.md body to keep generated skills bounded. */
|
|
22
|
+
export const MAX_MANAGED_SKILL_BYTES = 64_000;
|
|
23
|
+
|
|
24
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
|
|
25
|
+
|
|
26
|
+
/** Resolve the isolated managed-skills directory (`~/.omp/agent/managed-skills`). */
|
|
27
|
+
export function getManagedSkillsDir(home: string = os.homedir()): string {
|
|
28
|
+
return path.join(home, SOURCE_PATHS.native.userAgent, "managed-skills");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Validate + normalize a managed-skill name. Throws on anything outside the
|
|
33
|
+
* strict allowlist so a bad name can never escape `getManagedSkillsDir()`
|
|
34
|
+
* (blocks `..`, slashes, empty, and uppercase).
|
|
35
|
+
*/
|
|
36
|
+
export function sanitizeSkillName(raw: string): string {
|
|
37
|
+
const name = raw.trim().toLowerCase();
|
|
38
|
+
if (!SKILL_NAME_PATTERN.test(name)) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Invalid skill name "${raw}". Use lowercase letters, digits, and hyphens (1-64 chars, starting with a letter or digit).`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Whether `name` is a safe managed-skill name (the exact post-sanitize shape).
|
|
48
|
+
* Used to validate names read from disk at discovery time — a managed
|
|
49
|
+
* `SKILL.md` whose `frontmatter.name` was not produced by `sanitizeSkillName`
|
|
50
|
+
* (e.g. hand-placed) must not render unescaped into the system prompt.
|
|
51
|
+
*/
|
|
52
|
+
export function isValidManagedSkillName(name: string): boolean {
|
|
53
|
+
return SKILL_NAME_PATTERN.test(name);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Neutralize a machine-generated managed-skill description so it cannot break
|
|
58
|
+
* out of the system prompt's `<skills>` listing. Managed descriptions are
|
|
59
|
+
* generated from prior task content and persist across sessions, so this is a
|
|
60
|
+
* trust boundary: strip control/format chars, angle brackets (`<system-directive>`
|
|
61
|
+
* / `</skills>`), and Markdown fence delimiters (backticks, `~~~`), then collapse
|
|
62
|
+
* to a single line. Applied on BOTH write and read so existing files are safe too.
|
|
63
|
+
*/
|
|
64
|
+
export function sanitizeManagedDescription(raw: string): string {
|
|
65
|
+
return raw
|
|
66
|
+
.replace(/[\p{Cc}\p{Cf}]/gu, " ")
|
|
67
|
+
.replace(/[<>`]/g, "")
|
|
68
|
+
.replace(/~{2,}/g, "~")
|
|
69
|
+
.replace(/\s+/g, " ")
|
|
70
|
+
.trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Serialize the minimal `name`/`description` frontmatter block via the repo's
|
|
75
|
+
* YAML helper (round-trips through `parseFrontmatter`).
|
|
76
|
+
*/
|
|
77
|
+
export function toSkillFrontmatter(name: string, description: string): string {
|
|
78
|
+
const frontmatter = YAML.stringify(
|
|
79
|
+
{ name, description: sanitizeManagedDescription(description) },
|
|
80
|
+
null,
|
|
81
|
+
2,
|
|
82
|
+
).trimEnd();
|
|
83
|
+
return `---\n${frontmatter}\n---\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface WriteManagedSkillInput {
|
|
87
|
+
action: "create" | "update";
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
body: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Serialize create/update/delete on the same skill name. Both tools are
|
|
95
|
+
* non-exclusive, so a parallel tool batch in one turn can run two mutations on
|
|
96
|
+
* the same skill at once (e.g. an update observing the file mid-delete). This
|
|
97
|
+
* per-name promise chain runs same-skill mutations in submission order while
|
|
98
|
+
* different names still proceed in parallel. In-process only; cross-process
|
|
99
|
+
* races are out of scope.
|
|
100
|
+
*/
|
|
101
|
+
const skillMutationChains = new Map<string, Promise<unknown>>();
|
|
102
|
+
function serializeSkillMutation<T>(name: string, op: () => Promise<T>): Promise<T> {
|
|
103
|
+
const prev = skillMutationChains.get(name) ?? Promise.resolve();
|
|
104
|
+
const run = prev.then(op, op);
|
|
105
|
+
const guarded = run.catch(() => {});
|
|
106
|
+
skillMutationChains.set(name, guarded);
|
|
107
|
+
void guarded.finally(() => {
|
|
108
|
+
if (skillMutationChains.get(name) === guarded) skillMutationChains.delete(name);
|
|
109
|
+
});
|
|
110
|
+
return run;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Reject when the managed-skills root itself is a symlink. lstat on a child
|
|
115
|
+
* follows intermediate components, so a symlinked root would let an otherwise
|
|
116
|
+
* valid name write/delete outside the isolated directory (e.g. onto authored
|
|
117
|
+
* skills). Checked before composing any child path.
|
|
118
|
+
*/
|
|
119
|
+
async function assertManagedRootSafe(): Promise<void> {
|
|
120
|
+
const rootStat = await fs.lstat(getManagedSkillsDir()).catch(err => {
|
|
121
|
+
if (isEnoent(err)) return null;
|
|
122
|
+
throw err;
|
|
123
|
+
});
|
|
124
|
+
if (rootStat?.isSymbolicLink()) {
|
|
125
|
+
throw new Error("The managed-skills root is a symlink; refusing to operate outside the managed directory.");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const UPDATE_FILE_OPEN_FLAGS = fsConstants.O_WRONLY | fsConstants.O_NOFOLLOW;
|
|
130
|
+
|
|
131
|
+
function assertManagedSkillFileSafeForUpdate(name: string, fileStat: Stats): void {
|
|
132
|
+
if (!fileStat.isFile()) {
|
|
133
|
+
throw new Error(`Managed skill "${name}" SKILL.md is not a regular file; refusing to overwrite it.`);
|
|
134
|
+
}
|
|
135
|
+
if (fileStat.nlink > 1) {
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Managed skill "${name}" SKILL.md has ${fileStat.nlink} hard links; refusing to overwrite a file that may be user-authored elsewhere.`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function openManagedSkillFileForUpdate(name: string, file: string) {
|
|
143
|
+
try {
|
|
144
|
+
return await fs.open(file, UPDATE_FILE_OPEN_FLAGS);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if ((err as { code?: string }).code === "ELOOP") {
|
|
147
|
+
throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);
|
|
148
|
+
}
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Create or update a managed `SKILL.md`. Returns the resolved file path. */
|
|
154
|
+
export async function writeManagedSkill(input: WriteManagedSkillInput): Promise<{ path: string }> {
|
|
155
|
+
const name = sanitizeSkillName(input.name);
|
|
156
|
+
const description = sanitizeManagedDescription(input.description);
|
|
157
|
+
const body = input.body.trim();
|
|
158
|
+
// Reject empty content: an all-whitespace/control description sanitizes to ""
|
|
159
|
+
// and the `requireDescription` discovery scan then silently drops the skill,
|
|
160
|
+
// so the tool would report success for a skill that never appears.
|
|
161
|
+
if (!description) {
|
|
162
|
+
throw new Error(`Managed skill "${name}" needs a non-empty description.`);
|
|
163
|
+
}
|
|
164
|
+
if (!body) {
|
|
165
|
+
throw new Error(`Managed skill "${name}" needs a non-empty body.`);
|
|
166
|
+
}
|
|
167
|
+
const content = `${toSkillFrontmatter(name, description)}\n${body}\n`;
|
|
168
|
+
// Cap the UTF-8 byte size of the FINAL file (body + description + frontmatter),
|
|
169
|
+
// not the UTF-16 code-unit length of the body alone.
|
|
170
|
+
const bytes = Buffer.byteLength(content, "utf8");
|
|
171
|
+
if (bytes > MAX_MANAGED_SKILL_BYTES) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Managed skill is ${bytes} bytes; the limit is ${MAX_MANAGED_SKILL_BYTES}. Trim the body or description.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return serializeSkillMutation(name, async () => {
|
|
177
|
+
await assertManagedRootSafe();
|
|
178
|
+
const dir = path.join(getManagedSkillsDir(), name);
|
|
179
|
+
const file = path.join(dir, "SKILL.md");
|
|
180
|
+
// Reject a symlinked skill directory: an intermediate symlink would let the
|
|
181
|
+
// write escape the isolated managed root. lstat does not follow the final
|
|
182
|
+
// component, so a symlinked `dir` is caught here.
|
|
183
|
+
const dirStat = await fs.lstat(dir).catch(err => {
|
|
184
|
+
if (isEnoent(err)) return null;
|
|
185
|
+
throw err;
|
|
186
|
+
});
|
|
187
|
+
if (dirStat?.isSymbolicLink()) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`Managed skill "${name}" resolves through a symlink; refusing to write outside the managed directory.`,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
if (input.action === "create") {
|
|
193
|
+
await fs.mkdir(dir, { recursive: true });
|
|
194
|
+
// O_CREAT|O_EXCL ("wx"): atomic create that fails if the file already
|
|
195
|
+
// exists (closing the check-then-write race) and refuses a symlinked SKILL.md.
|
|
196
|
+
try {
|
|
197
|
+
await fs.writeFile(file, content, { flag: "wx" });
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if ((err as { code?: string }).code === "EEXIST") {
|
|
200
|
+
throw new Error(`Managed skill "${name}" already exists. Use action "update" to change it.`);
|
|
201
|
+
}
|
|
202
|
+
throw err;
|
|
203
|
+
}
|
|
204
|
+
return { path: file };
|
|
205
|
+
}
|
|
206
|
+
// update: the file must already exist, be a plain managed file, and must
|
|
207
|
+
// not share an inode with a user-authored file via hard link. Open the
|
|
208
|
+
// checked file handle before truncating so a path swap after lstat cannot
|
|
209
|
+
// redirect the write into a symlink or newly hard-linked target.
|
|
210
|
+
const fileStat = await fs.lstat(file).catch(err => {
|
|
211
|
+
if (isEnoent(err)) return null;
|
|
212
|
+
throw err;
|
|
213
|
+
});
|
|
214
|
+
if (fileStat === null) {
|
|
215
|
+
throw new Error(`Managed skill "${name}" does not exist. Use action "create" to add it.`);
|
|
216
|
+
}
|
|
217
|
+
if (fileStat.isSymbolicLink()) {
|
|
218
|
+
throw new Error(`Managed skill "${name}" SKILL.md is a symlink; refusing to overwrite it.`);
|
|
219
|
+
}
|
|
220
|
+
assertManagedSkillFileSafeForUpdate(name, fileStat);
|
|
221
|
+
const handle = await openManagedSkillFileForUpdate(name, file);
|
|
222
|
+
try {
|
|
223
|
+
const openStat = await handle.stat();
|
|
224
|
+
assertManagedSkillFileSafeForUpdate(name, openStat);
|
|
225
|
+
await handle.truncate(0);
|
|
226
|
+
await handle.writeFile(content);
|
|
227
|
+
} finally {
|
|
228
|
+
await handle.close();
|
|
229
|
+
}
|
|
230
|
+
return { path: file };
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Delete a managed skill directory. Throws when it does not exist. */
|
|
235
|
+
export async function deleteManagedSkill(name: string): Promise<void> {
|
|
236
|
+
const safe = sanitizeSkillName(name);
|
|
237
|
+
await serializeSkillMutation(safe, async () => {
|
|
238
|
+
await assertManagedRootSafe();
|
|
239
|
+
const dir = path.join(getManagedSkillsDir(), safe);
|
|
240
|
+
// Refuse to follow a symlinked skill directory (rm would delete the target).
|
|
241
|
+
const dirStat = await fs.lstat(dir).catch(err => {
|
|
242
|
+
if (isEnoent(err)) return null;
|
|
243
|
+
throw err;
|
|
244
|
+
});
|
|
245
|
+
if (dirStat?.isSymbolicLink()) {
|
|
246
|
+
throw new Error(`Managed skill "${safe}" is a symlink; refusing to delete outside the managed directory.`);
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
await fs.rm(dir, { recursive: true });
|
|
250
|
+
} catch (err) {
|
|
251
|
+
if (isEnoent(err)) {
|
|
252
|
+
throw new Error(`Managed skill "${safe}" does not exist.`);
|
|
253
|
+
}
|
|
254
|
+
throw err;
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import type { ExtensionAPI, ExtensionContext } from "../extensibility/extensions";
|
|
3
|
-
import type { SessionEntry } from "../session/session-
|
|
3
|
+
import type { SessionEntry } from "../session/session-entries";
|
|
4
4
|
import type { TruncationResult } from "../session/streaming-output";
|
|
5
5
|
|
|
6
6
|
export type MetricDirection = "lower" | "higher";
|
package/src/cli/args.ts
CHANGED
|
@@ -53,8 +53,19 @@ export interface Args {
|
|
|
53
53
|
approvalMode?: "always-ask" | "write" | "yolo";
|
|
54
54
|
messages: string[];
|
|
55
55
|
fileArgs: string[];
|
|
56
|
-
/**
|
|
56
|
+
/** Extension-registered flags this parse recognized — name to value. */
|
|
57
57
|
unknownFlags: Map<string, boolean | string>;
|
|
58
|
+
/**
|
|
59
|
+
* `--`/`-` prefixed tokens this parse could not match against any built-in
|
|
60
|
+
* or {@link extensionFlags} entry. The startup parse runs *before*
|
|
61
|
+
* extensions load, so it always lists every extension-registered flag here;
|
|
62
|
+
* the post-extension reparse in {@link applyExtensionFlags} clears those
|
|
63
|
+
* once the real flag set is known. Anything still present after that
|
|
64
|
+
* reparse is a genuine typo or stale flag and {@link reportUnrecognizedFlags}
|
|
65
|
+
* surfaces it as a hard error so the agent does not silently start a
|
|
66
|
+
* session with the misparsed positionals as a prompt (issue #2459).
|
|
67
|
+
*/
|
|
68
|
+
unrecognizedFlags: string[];
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
|
|
@@ -67,12 +78,23 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
67
78
|
messages: [],
|
|
68
79
|
fileArgs: [],
|
|
69
80
|
unknownFlags: new Map(),
|
|
81
|
+
unrecognizedFlags: [],
|
|
70
82
|
};
|
|
71
83
|
|
|
84
|
+
let sawSeparator = false;
|
|
72
85
|
for (let i = 0; i < args.length; i++) {
|
|
73
86
|
let arg = args[i];
|
|
74
87
|
const flagIndex = i;
|
|
75
88
|
|
|
89
|
+
// POSIX positional separator: once `--` lands, every remaining token is
|
|
90
|
+
// a positional regardless of shape. Without this, a flag-looking message
|
|
91
|
+
// (`omp -p -- --explain-this`) would be re-validated by the loop below
|
|
92
|
+
// and rejected by the unknown-flag guard (#2461 review).
|
|
93
|
+
if (sawSeparator) {
|
|
94
|
+
result.messages.push(arg);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
76
98
|
// Support --flag=value syntax (e.g. --tools=ask,read). The value is
|
|
77
99
|
// spliced in as the next token so value-consuming flags pick it up via
|
|
78
100
|
// `args[++i]`; a non-consuming flag (e.g. a boolean) leaves it behind and
|
|
@@ -229,8 +251,22 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
229
251
|
result.skills = args[++i].split(",").map(s => s.trim());
|
|
230
252
|
} else if (arg.startsWith("@")) {
|
|
231
253
|
result.fileArgs.push(arg.slice(1)); // Remove @ prefix
|
|
232
|
-
} else if (!arg.startsWith("-")) {
|
|
254
|
+
} else if (!arg.startsWith("-") || arg === "-") {
|
|
255
|
+
// Plain positional or lone `-` (stdin marker) — pass through as a
|
|
256
|
+
// message rather than flagging it.
|
|
233
257
|
result.messages.push(arg);
|
|
258
|
+
} else if (arg === "--") {
|
|
259
|
+
// POSIX positional separator: drop the token and switch the loop
|
|
260
|
+
// into "everything from here is a positional" mode. The guard at
|
|
261
|
+
// the top of the loop body handles the remaining tokens.
|
|
262
|
+
sawSeparator = true;
|
|
263
|
+
} else {
|
|
264
|
+
// Flag-shaped (`-x`, `--name`) but unrecognized at this parse. Record
|
|
265
|
+
// it so the post-extension reparse can decide whether to surface it
|
|
266
|
+
// as a hard error. `--flag=value` already split `value` into the next
|
|
267
|
+
// slot; the standard "drop unconsumed equals value" guard below
|
|
268
|
+
// removes it so it does not leak into messages (issue #2459).
|
|
269
|
+
result.unrecognizedFlags.push(arg);
|
|
234
270
|
}
|
|
235
271
|
// Drop an unconsumed `--flag=value` value (e.g. a boolean flag): when no
|
|
236
272
|
// branch advanced past the spliced token, remove it so it does not fall
|
|
@@ -243,6 +279,24 @@ export function parseArgs(inputArgs: string[], extensionFlags?: Map<string, { ty
|
|
|
243
279
|
return result;
|
|
244
280
|
}
|
|
245
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Emit a stderr error listing the unrecognized flags and return `true` when
|
|
284
|
+
* there were any. Caller is expected to exit with a non-zero status. Splitting
|
|
285
|
+
* the print from the exit keeps the helper unit-testable without forking a
|
|
286
|
+
* process (issue #2459).
|
|
287
|
+
*/
|
|
288
|
+
export function reportUnrecognizedFlags(
|
|
289
|
+
args: Pick<Args, "unrecognizedFlags">,
|
|
290
|
+
write: (text: string) => void = text => process.stderr.write(text),
|
|
291
|
+
): boolean {
|
|
292
|
+
if (args.unrecognizedFlags.length === 0) return false;
|
|
293
|
+
const flags = args.unrecognizedFlags;
|
|
294
|
+
const plural = flags.length === 1 ? "" : "s";
|
|
295
|
+
write(`${chalk.red(`Error: unknown flag${plural}: ${flags.join(", ")}`)}\n`);
|
|
296
|
+
write(`Run \`${APP_NAME} --help\` for available flags.\n`);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
246
300
|
export function getExtraHelpText(): string {
|
|
247
301
|
return `${chalk.bold("Environment Variables:")}
|
|
248
302
|
${chalk.dim("# Core Providers")}
|
|
@@ -2,7 +2,8 @@ import { ProcessTerminal, TUI } from "@oh-my-pi/pi-tui";
|
|
|
2
2
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { SessionSelectorComponent } from "../modes/components/session-selector";
|
|
4
4
|
import { HistoryStorage } from "../session/history-storage";
|
|
5
|
-
import {
|
|
5
|
+
import type { SessionInfo } from "../session/session-listing";
|
|
6
|
+
import { SessionManager } from "../session/session-manager";
|
|
6
7
|
import { FileSessionStorage } from "../session/session-storage";
|
|
7
8
|
|
|
8
9
|
/**
|
package/src/cli/setup-cli.ts
CHANGED
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
* Handles `omp setup` for onboarding and `omp setup <component>` for optional dependencies.
|
|
5
5
|
*/
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { $which, APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils";
|
|
7
|
+
import { $which, APP_NAME, getProjectDir, getPythonEnvDir } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import { $ } from "bun";
|
|
9
9
|
import chalk from "chalk";
|
|
10
|
+
import { Settings, settings } from "../config/settings";
|
|
10
11
|
import { theme } from "../modes/theme/theme";
|
|
12
|
+
import { downloadSttModel, isSttModelCached } from "../stt/downloader";
|
|
13
|
+
import { isSttModelKey, STT_MODEL_OPTIONS } from "../stt/models";
|
|
14
|
+
import { detectRecorder, ensureRecorder } from "../stt/recorder";
|
|
15
|
+
import { downloadTtsModel, isTtsLocalModelKey, isTtsModelCached, TTS_LOCAL_MODEL_OPTIONS } from "../tts";
|
|
16
|
+
import { selectSetupModel } from "./setup-model-picker";
|
|
11
17
|
|
|
12
|
-
export type SetupComponent = "python" | "
|
|
18
|
+
export type SetupComponent = "python" | "speech";
|
|
13
19
|
|
|
14
20
|
export interface SetupCommandArgs {
|
|
15
21
|
component: SetupComponent;
|
|
@@ -19,7 +25,7 @@ export interface SetupCommandArgs {
|
|
|
19
25
|
};
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
const VALID_COMPONENTS: SetupComponent[] = ["python", "
|
|
28
|
+
const VALID_COMPONENTS: SetupComponent[] = ["python", "speech"];
|
|
23
29
|
|
|
24
30
|
const MANAGED_PYTHON_ENV = getPythonEnvDir();
|
|
25
31
|
|
|
@@ -114,8 +120,8 @@ export async function runSetupCommand(cmd: SetupCommandArgs): Promise<void> {
|
|
|
114
120
|
case "python":
|
|
115
121
|
await handlePythonSetup(cmd.flags);
|
|
116
122
|
break;
|
|
117
|
-
case "
|
|
118
|
-
await
|
|
123
|
+
case "speech":
|
|
124
|
+
await handleSpeechSetup(cmd.flags);
|
|
119
125
|
break;
|
|
120
126
|
}
|
|
121
127
|
}
|
|
@@ -149,58 +155,153 @@ async function handlePythonSetup(flags: { json?: boolean; check?: boolean }): Pr
|
|
|
149
155
|
process.exit(1);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
/**
|
|
159
|
+
* One installable speech dependency. `isReady`/`status` are read-only probes;
|
|
160
|
+
* `pick` (optional) lets an interactive user choose + persist a model; `ensure`
|
|
161
|
+
* performs the download, streaming a normalized progress event.
|
|
162
|
+
*/
|
|
163
|
+
interface SpeechComponent {
|
|
164
|
+
name: string;
|
|
165
|
+
isReady(): Promise<boolean>;
|
|
166
|
+
status(): Promise<string>;
|
|
167
|
+
pick?(): Promise<boolean>;
|
|
168
|
+
ensure(onProgress: (progress: { stage: string; percent?: number }) => void): Promise<void>;
|
|
169
|
+
}
|
|
155
170
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
171
|
+
function buildSpeechComponents(): SpeechComponent[] {
|
|
172
|
+
return [
|
|
173
|
+
{
|
|
174
|
+
name: "Recorder",
|
|
175
|
+
isReady: async () => detectRecorder() !== null,
|
|
176
|
+
status: async () => {
|
|
177
|
+
const recorder = detectRecorder();
|
|
178
|
+
return recorder ? `${recorder.tool} (${recorder.bin})` : "none — ffmpeg will be downloaded";
|
|
179
|
+
},
|
|
180
|
+
ensure: async onProgress => {
|
|
181
|
+
await ensureRecorder(onProgress);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "Speech-to-Text model",
|
|
186
|
+
isReady: () => isSttModelCached(settings.get("stt.modelName")),
|
|
187
|
+
status: async () => {
|
|
188
|
+
const key = settings.get("stt.modelName");
|
|
189
|
+
return (await isSttModelCached(key)) ? key : `${key} — not downloaded`;
|
|
190
|
+
},
|
|
191
|
+
pick: async () => {
|
|
192
|
+
const chosen = await selectSetupModel(
|
|
193
|
+
"Speech-to-Text model",
|
|
194
|
+
[...STT_MODEL_OPTIONS],
|
|
195
|
+
settings.get("stt.modelName"),
|
|
196
|
+
);
|
|
197
|
+
if (chosen === null) return false;
|
|
198
|
+
if (isSttModelKey(chosen)) {
|
|
199
|
+
settings.set("stt.modelName", chosen);
|
|
200
|
+
await settings.flush();
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
},
|
|
204
|
+
ensure: onProgress =>
|
|
205
|
+
downloadSttModel(settings.get("stt.modelName"), progress =>
|
|
206
|
+
onProgress({ stage: `Downloading ${progress.label} model`, percent: progress.percent }),
|
|
207
|
+
),
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "Text-to-Speech model",
|
|
211
|
+
isReady: () => isTtsModelCached(settings.get("tts.localModel")),
|
|
212
|
+
status: async () => {
|
|
213
|
+
const key = settings.get("tts.localModel");
|
|
214
|
+
return (await isTtsModelCached(key)) ? key : `${key} — model/runtime not installed`;
|
|
215
|
+
},
|
|
216
|
+
pick: async () => {
|
|
217
|
+
const chosen = await selectSetupModel(
|
|
218
|
+
"Text-to-Speech model",
|
|
219
|
+
[...TTS_LOCAL_MODEL_OPTIONS],
|
|
220
|
+
settings.get("tts.localModel"),
|
|
221
|
+
);
|
|
222
|
+
if (chosen === null) return false;
|
|
223
|
+
if (isTtsLocalModelKey(chosen)) {
|
|
224
|
+
settings.set("tts.localModel", chosen);
|
|
225
|
+
await settings.flush();
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
229
|
+
ensure: async onProgress => {
|
|
230
|
+
const ok = await downloadTtsModel(settings.get("tts.localModel"), progress =>
|
|
231
|
+
onProgress({ stage: progress.stage, percent: progress.percent }),
|
|
232
|
+
);
|
|
233
|
+
if (!ok) throw new Error("Failed to download the local text-to-speech model.");
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
];
|
|
237
|
+
}
|
|
161
238
|
|
|
162
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Unified `omp setup speech` flow. Drives every {@link SpeechComponent} through
|
|
241
|
+
* one path: report (`--json`/`--check`) or install (interactive pick + ensure
|
|
242
|
+
* with single-line progress; non-TTY skips pickers and installs configured
|
|
243
|
+
* values).
|
|
244
|
+
*/
|
|
245
|
+
async function handleSpeechSetup(flags: { json?: boolean; check?: boolean }): Promise<void> {
|
|
246
|
+
await Settings.init({ cwd: getProjectDir() });
|
|
247
|
+
const components = buildSpeechComponents();
|
|
163
248
|
|
|
164
|
-
if (
|
|
165
|
-
|
|
249
|
+
if (flags.json) {
|
|
250
|
+
const report: Record<string, { ready: boolean; status: string }> = {};
|
|
251
|
+
let allReady = true;
|
|
252
|
+
for (const component of components) {
|
|
253
|
+
const ready = await component.isReady();
|
|
254
|
+
if (!ready) allReady = false;
|
|
255
|
+
report[component.name] = { ready, status: await component.status() };
|
|
256
|
+
}
|
|
257
|
+
console.log(JSON.stringify(report, null, 2));
|
|
258
|
+
if (!allReady) process.exit(1);
|
|
166
259
|
return;
|
|
167
260
|
}
|
|
168
261
|
|
|
169
262
|
if (flags.check) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
console.error(chalk.yellow(`\n${theme.status.warning} No recording tool found`));
|
|
181
|
-
console.error(chalk.dim(status.recorder.installHint));
|
|
263
|
+
console.log(chalk.bold("Speech dependencies:"));
|
|
264
|
+
let allReady = true;
|
|
265
|
+
for (const component of components) {
|
|
266
|
+
const ready = await component.isReady();
|
|
267
|
+
if (!ready) allReady = false;
|
|
268
|
+
const mark = ready ? chalk.green("[ok]") : chalk.yellow("[missing]");
|
|
269
|
+
console.log(` ${mark} ${component.name}: ${await component.status()}`);
|
|
270
|
+
}
|
|
271
|
+
if (!allReady) process.exit(1);
|
|
272
|
+
return;
|
|
182
273
|
}
|
|
183
274
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
console.
|
|
191
|
-
|
|
275
|
+
const interactive = Boolean(process.stdout.isTTY);
|
|
276
|
+
for (const component of components) {
|
|
277
|
+
if (interactive && component.pick) {
|
|
278
|
+
await component.pick();
|
|
279
|
+
}
|
|
280
|
+
if (await component.isReady()) {
|
|
281
|
+
console.log(chalk.green(`${theme.status.success} ${component.name} ready`));
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
console.log(chalk.dim(`Preparing ${component.name}...`));
|
|
285
|
+
try {
|
|
286
|
+
await component.ensure(progress => {
|
|
287
|
+
const percent = typeof progress.percent === "number" ? ` (${progress.percent}%)` : "";
|
|
288
|
+
process.stdout.write(`\r${chalk.dim(`${progress.stage}${percent}`)}\x1b[K`);
|
|
289
|
+
});
|
|
290
|
+
process.stdout.write("\n");
|
|
291
|
+
} catch (err) {
|
|
292
|
+
process.stdout.write("\n");
|
|
293
|
+
const msg = err instanceof Error ? err.message : `Failed to set up ${component.name}`;
|
|
294
|
+
console.error(chalk.red(`${theme.status.error} ${msg}`));
|
|
192
295
|
process.exit(1);
|
|
193
296
|
}
|
|
194
297
|
}
|
|
195
298
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
process.exit(1);
|
|
203
|
-
}
|
|
299
|
+
console.log(chalk.green(`\n${theme.status.success} Speech is ready`));
|
|
300
|
+
console.log(
|
|
301
|
+
chalk.dim(
|
|
302
|
+
"Enable speech-to-text via stt.enabled, then hold Space to talk (or bind app.stt.toggle); enable the speech-generation tool via speechgen.enabled; speak replies aloud via speech.enabled.",
|
|
303
|
+
),
|
|
304
|
+
);
|
|
204
305
|
}
|
|
205
306
|
|
|
206
307
|
/**
|
|
@@ -215,7 +316,7 @@ ${chalk.bold("Usage:")}
|
|
|
215
316
|
|
|
216
317
|
${chalk.bold("Components:")}
|
|
217
318
|
python Verify a Python 3 interpreter is reachable for code execution
|
|
218
|
-
|
|
319
|
+
speech Pick + download the speech-to-text and text-to-speech models and an audio recorder
|
|
219
320
|
|
|
220
321
|
${chalk.bold("Options:")}
|
|
221
322
|
-c, --check Check if dependencies are installed without installing
|
|
@@ -224,8 +325,8 @@ ${chalk.bold("Options:")}
|
|
|
224
325
|
${chalk.bold("Examples:")}
|
|
225
326
|
${APP_NAME} setup Run the onboarding wizard
|
|
226
327
|
${APP_NAME} setup python Check Python execution dependencies
|
|
227
|
-
${APP_NAME} setup
|
|
228
|
-
${APP_NAME} setup
|
|
328
|
+
${APP_NAME} setup speech Set up speech (pick STT + TTS models, install a recorder)
|
|
329
|
+
${APP_NAME} setup speech --check Check if speech dependencies are available
|
|
229
330
|
${APP_NAME} setup python --check Check if Python execution is available
|
|
230
331
|
`);
|
|
231
332
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Standalone TUI model picker used by `omp setup speech`.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors {@link ./session-picker.ts} for the standalone-TUI lifecycle: spin up
|
|
5
|
+
* a one-shot {@link TUI} over a {@link SelectList}, resolve on select/cancel, and
|
|
6
|
+
* tear the UI down. The standalone TUI auto-renders on input, so no manual
|
|
7
|
+
* render wiring is needed beyond `addChild`/`setFocus`/`start`.
|
|
8
|
+
*/
|
|
9
|
+
import { ProcessTerminal, type SelectItem, SelectList, TUI } from "@oh-my-pi/pi-tui";
|
|
10
|
+
import { getSelectListTheme } from "../modes/theme/theme";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Show a single-column model picker and resolve with the chosen item's value,
|
|
14
|
+
* or `null` if the user cancelled. `currentValue` pre-selects the matching row.
|
|
15
|
+
*/
|
|
16
|
+
export async function selectSetupModel(
|
|
17
|
+
title: string,
|
|
18
|
+
items: SelectItem[],
|
|
19
|
+
currentValue: string,
|
|
20
|
+
): Promise<string | null> {
|
|
21
|
+
const { promise, resolve } = Promise.withResolvers<string | null>();
|
|
22
|
+
const ui = new TUI(new ProcessTerminal());
|
|
23
|
+
let resolved = false;
|
|
24
|
+
|
|
25
|
+
const finish = (value: string | null): void => {
|
|
26
|
+
if (resolved) return;
|
|
27
|
+
resolved = true;
|
|
28
|
+
ui.stop();
|
|
29
|
+
resolve(value);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const list = new SelectList(items, Math.min(items.length, 10), getSelectListTheme());
|
|
33
|
+
const currentIndex = items.findIndex(item => item.value === currentValue);
|
|
34
|
+
if (currentIndex >= 0) list.setSelectedIndex(currentIndex);
|
|
35
|
+
list.onSelect = item => finish(item.value);
|
|
36
|
+
list.onCancel = () => finish(null);
|
|
37
|
+
|
|
38
|
+
process.stdout.write(`${title}\n`);
|
|
39
|
+
ui.addChild(list);
|
|
40
|
+
ui.setFocus(list);
|
|
41
|
+
ui.start();
|
|
42
|
+
return promise;
|
|
43
|
+
}
|