@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.1

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.
Files changed (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -1,52 +1,16 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
- import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
2
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { Type } from "@sinclair/typebox";
6
5
  import analysisSystemPrompt from "../../commit/prompts/analysis-system.md" with { type: "text" };
7
6
  import analysisUserPrompt from "../../commit/prompts/analysis-user.md" with { type: "text" };
8
- import type { ChangelogCategory, ConventionalAnalysis } from "../../commit/types";
7
+ import type { ConventionalAnalysis } from "../../commit/types";
9
8
  import { toReasoningEffort } from "../../thinking";
10
- import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
9
+ import { createConventionalAnalysisTool, parseConventionalAnalysisResponse } from "../shared-llm";
11
10
 
12
- const ConventionalAnalysisTool = {
13
- name: "create_conventional_analysis",
14
- description: "Analyze a diff and return conventional commit classification.",
15
- parameters: Type.Object({
16
- type: Type.Union([
17
- Type.Literal("feat"),
18
- Type.Literal("fix"),
19
- Type.Literal("refactor"),
20
- Type.Literal("docs"),
21
- Type.Literal("test"),
22
- Type.Literal("chore"),
23
- Type.Literal("style"),
24
- Type.Literal("perf"),
25
- Type.Literal("build"),
26
- Type.Literal("ci"),
27
- Type.Literal("revert"),
28
- ]),
29
- scope: Type.Union([Type.String(), Type.Null()]),
30
- details: Type.Array(
31
- Type.Object({
32
- text: Type.String(),
33
- changelog_category: Type.Optional(
34
- Type.Union([
35
- Type.Literal("Added"),
36
- Type.Literal("Changed"),
37
- Type.Literal("Fixed"),
38
- Type.Literal("Deprecated"),
39
- Type.Literal("Removed"),
40
- Type.Literal("Security"),
41
- Type.Literal("Breaking Changes"),
42
- ]),
43
- ),
44
- user_visible: Type.Optional(Type.Boolean()),
45
- }),
46
- ),
47
- issue_refs: Type.Array(Type.String()),
48
- }),
49
- };
11
+ const ConventionalAnalysisTool = createConventionalAnalysisTool(
12
+ "Analyze a diff and return conventional commit classification.",
13
+ );
50
14
 
51
15
  export interface ConventionalAnalysisInput {
52
16
  model: Model<Api>;
@@ -96,27 +60,5 @@ export async function generateConventionalAnalysis({
96
60
  { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
97
61
  );
98
62
 
99
- return parseAnalysisFromResponse(response);
100
- }
101
-
102
- function parseAnalysisFromResponse(message: AssistantMessage): ConventionalAnalysis {
103
- const toolCall = extractToolCall(message, "create_conventional_analysis");
104
- if (toolCall) {
105
- const parsed = validateToolCall([ConventionalAnalysisTool], toolCall) as {
106
- type: ConventionalAnalysis["type"];
107
- scope: string | null;
108
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
109
- issue_refs: string[];
110
- };
111
- return normalizeAnalysis(parsed);
112
- }
113
-
114
- const text = extractTextContent(message);
115
- const parsed = parseJsonPayload(text) as {
116
- type: ConventionalAnalysis["type"];
117
- scope: string | null;
118
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
119
- issue_refs: string[];
120
- };
121
- return normalizeAnalysis(parsed);
63
+ return parseConventionalAnalysisResponse(response, ConventionalAnalysisTool);
122
64
  }
@@ -1,52 +1,14 @@
1
1
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import type { Api, AssistantMessage, Model } from "@oh-my-pi/pi-ai";
3
- import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
2
+ import type { Api, Model } from "@oh-my-pi/pi-ai";
3
+ import { completeSimple } from "@oh-my-pi/pi-ai";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
- import { Type } from "@sinclair/typebox";
6
5
  import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
7
6
  import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
8
- import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
7
+ import type { ConventionalAnalysis, FileObservation } from "../../commit/types";
9
8
  import { toReasoningEffort } from "../../thinking";
10
- import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "../utils";
9
+ import { createConventionalAnalysisTool, parseConventionalAnalysisResponse } from "../shared-llm";
11
10
 
12
- const ReduceTool = {
13
- name: "create_conventional_analysis",
14
- description: "Synthesize file observations into a conventional commit analysis.",
15
- parameters: Type.Object({
16
- type: Type.Union([
17
- Type.Literal("feat"),
18
- Type.Literal("fix"),
19
- Type.Literal("refactor"),
20
- Type.Literal("docs"),
21
- Type.Literal("test"),
22
- Type.Literal("chore"),
23
- Type.Literal("style"),
24
- Type.Literal("perf"),
25
- Type.Literal("build"),
26
- Type.Literal("ci"),
27
- Type.Literal("revert"),
28
- ]),
29
- scope: Type.Union([Type.String(), Type.Null()]),
30
- details: Type.Array(
31
- Type.Object({
32
- text: Type.String(),
33
- changelog_category: Type.Optional(
34
- Type.Union([
35
- Type.Literal("Added"),
36
- Type.Literal("Changed"),
37
- Type.Literal("Fixed"),
38
- Type.Literal("Deprecated"),
39
- Type.Literal("Removed"),
40
- Type.Literal("Security"),
41
- Type.Literal("Breaking Changes"),
42
- ]),
43
- ),
44
- user_visible: Type.Optional(Type.Boolean()),
45
- }),
46
- ),
47
- issue_refs: Type.Array(Type.String()),
48
- }),
49
- };
11
+ const ReduceTool = createConventionalAnalysisTool("Synthesize file observations into a conventional commit analysis.");
50
12
 
51
13
  export interface ReducePhaseInput {
52
14
  model: Model<Api>;
@@ -83,26 +45,5 @@ export async function runReducePhase({
83
45
  { apiKey, maxTokens: 2400, reasoning: toReasoningEffort(thinkingLevel) },
84
46
  );
85
47
 
86
- return parseAnalysisResponse(response);
87
- }
88
-
89
- function parseAnalysisResponse(message: AssistantMessage): ConventionalAnalysis {
90
- const toolCall = extractToolCall(message, "create_conventional_analysis");
91
- if (toolCall) {
92
- const parsed = validateToolCall([ReduceTool], toolCall) as {
93
- type: ConventionalAnalysis["type"];
94
- scope: string | null;
95
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
96
- issue_refs: string[];
97
- };
98
- return normalizeAnalysis(parsed);
99
- }
100
- const text = extractTextContent(message);
101
- const parsed = parseJsonPayload(text) as {
102
- type: ConventionalAnalysis["type"];
103
- scope: string | null;
104
- details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
105
- issue_refs: string[];
106
- };
107
- return normalizeAnalysis(parsed);
48
+ return parseConventionalAnalysisResponse(response, ReduceTool);
108
49
  }
@@ -25,8 +25,8 @@ import type { CommitCommandArgs, ConventionalAnalysis } from "./types";
25
25
 
26
26
  const SUMMARY_MAX_CHARS = 72;
27
27
  const RECENT_COMMITS_COUNT = 8;
28
- let _typesDescription: string | undefined;
29
- const TYPES_DESCRIPTION = (): string => (_typesDescription ??= prompt.render(typesDescriptionPrompt));
28
+ let typesDescription: string | undefined;
29
+ const TYPES_DESCRIPTION = (): string => (typesDescription ??= prompt.render(typesDescriptionPrompt));
30
30
 
31
31
  /**
32
32
  * Execute the omp commit pipeline for staged changes.
@@ -0,0 +1,89 @@
1
+ import type { AssistantMessage } from "@oh-my-pi/pi-ai";
2
+ import { validateToolCall } from "@oh-my-pi/pi-ai";
3
+ import { Type } from "@sinclair/typebox";
4
+ import type { ChangelogCategory, ConventionalAnalysis } from "./types";
5
+ import { extractTextContent, extractToolCall, normalizeAnalysis, parseJsonPayload } from "./utils";
6
+
7
+ /**
8
+ * Shared TypeBox schema for the `create_conventional_analysis` tool used by
9
+ * both the single-pass analysis call and the map-reduce reduce phase. Schemas
10
+ * are identical across phases — only the surrounding tool `description`
11
+ * differs to reflect the input the phase is summarizing.
12
+ */
13
+ export const conventionalAnalysisParameters = Type.Object({
14
+ type: Type.Union([
15
+ Type.Literal("feat"),
16
+ Type.Literal("fix"),
17
+ Type.Literal("refactor"),
18
+ Type.Literal("docs"),
19
+ Type.Literal("test"),
20
+ Type.Literal("chore"),
21
+ Type.Literal("style"),
22
+ Type.Literal("perf"),
23
+ Type.Literal("build"),
24
+ Type.Literal("ci"),
25
+ Type.Literal("revert"),
26
+ ]),
27
+ scope: Type.Union([Type.String(), Type.Null()]),
28
+ details: Type.Array(
29
+ Type.Object({
30
+ text: Type.String(),
31
+ changelog_category: Type.Optional(
32
+ Type.Union([
33
+ Type.Literal("Added"),
34
+ Type.Literal("Changed"),
35
+ Type.Literal("Fixed"),
36
+ Type.Literal("Deprecated"),
37
+ Type.Literal("Removed"),
38
+ Type.Literal("Security"),
39
+ Type.Literal("Breaking Changes"),
40
+ ]),
41
+ ),
42
+ user_visible: Type.Optional(Type.Boolean()),
43
+ }),
44
+ ),
45
+ issue_refs: Type.Array(Type.String()),
46
+ });
47
+
48
+ export interface ConventionalAnalysisTool {
49
+ name: "create_conventional_analysis";
50
+ description: string;
51
+ parameters: typeof conventionalAnalysisParameters;
52
+ }
53
+
54
+ /**
55
+ * Build a `create_conventional_analysis` tool descriptor. Phase-specific
56
+ * `description` text is the only thing that varies between callers.
57
+ */
58
+ export function createConventionalAnalysisTool(description: string): ConventionalAnalysisTool {
59
+ return {
60
+ name: "create_conventional_analysis",
61
+ description,
62
+ parameters: conventionalAnalysisParameters,
63
+ };
64
+ }
65
+
66
+ interface ParsedConventionalAnalysis {
67
+ type: ConventionalAnalysis["type"];
68
+ scope: string | null;
69
+ details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
70
+ issue_refs: string[];
71
+ }
72
+
73
+ /**
74
+ * Extract a {@link ConventionalAnalysis} from an assistant response, preferring
75
+ * a structured tool call and falling back to JSON embedded in text content.
76
+ */
77
+ export function parseConventionalAnalysisResponse(
78
+ message: AssistantMessage,
79
+ tool: ConventionalAnalysisTool,
80
+ ): ConventionalAnalysis {
81
+ const toolCall = extractToolCall(message, tool.name);
82
+ if (toolCall) {
83
+ const parsed = validateToolCall([tool], toolCall) as ParsedConventionalAnalysis;
84
+ return normalizeAnalysis(parsed);
85
+ }
86
+ const text = extractTextContent(message);
87
+ const parsed = parseJsonPayload(text) as ParsedConventionalAnalysis;
88
+ return normalizeAnalysis(parsed);
89
+ }
@@ -0,0 +1,210 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
4
+ import type { TSchema } from "@sinclair/typebox";
5
+ import { Value } from "@sinclair/typebox/value";
6
+ import type { ErrorObject } from "ajv";
7
+ import { JSONC, YAML } from "bun";
8
+
9
+ function migrateJsonToYml(jsonPath: string, ymlPath: string) {
10
+ try {
11
+ if (fs.existsSync(ymlPath)) return;
12
+ if (!fs.existsSync(jsonPath)) return;
13
+
14
+ const content = fs.readFileSync(jsonPath, "utf-8");
15
+ const parsed = JSON.parse(content);
16
+ if (!parsed) {
17
+ logger.warn("migrateJsonToYml: invalid json structure", { path: jsonPath });
18
+ return;
19
+ }
20
+ fs.writeFileSync(ymlPath, YAML.stringify(parsed, null, 2));
21
+ } catch (error) {
22
+ logger.warn("migrateJsonToYml: migration failed", { error: String(error) });
23
+ }
24
+ }
25
+
26
+ export interface IConfigFile<T> {
27
+ readonly id: string;
28
+ readonly schema: TSchema;
29
+ path?(): string;
30
+ load(): T | null;
31
+ invalidate?(): void;
32
+ }
33
+
34
+ export class ConfigError extends Error {
35
+ readonly #message: string;
36
+ constructor(
37
+ public readonly id: string,
38
+ public readonly schemaErrors: ErrorObject[] | null | undefined,
39
+ public readonly other?: { err: unknown; stage: string },
40
+ ) {
41
+ let messages: string[] | undefined;
42
+ let cause: Error | undefined;
43
+ let klass: string;
44
+
45
+ if (schemaErrors) {
46
+ klass = "Schema";
47
+ messages = schemaErrors.map(e => `${e.instancePath || "root"}: ${e.message}`);
48
+ } else if (other) {
49
+ klass = other.stage;
50
+ if (other.err instanceof Error) {
51
+ messages = [other.err.message];
52
+ cause = other.err;
53
+ } else {
54
+ messages = [String(other.err)];
55
+ }
56
+ } else {
57
+ klass = "Unknown";
58
+ }
59
+
60
+ const title = `Failed to load config file ${id}, ${klass} error:`;
61
+ let message: string;
62
+ switch (messages?.length ?? 0) {
63
+ case 0:
64
+ message = title.slice(0, -1);
65
+ break;
66
+ case 1:
67
+ message = `${title} ${messages![0]}`;
68
+ break;
69
+ default:
70
+ message = `${title}\n${messages!.map(m => ` - ${m}`).join("\n")}`;
71
+ break;
72
+ }
73
+
74
+ super(message, { cause });
75
+ this.name = "LoadError";
76
+ this.#message = message;
77
+ }
78
+
79
+ get message(): string {
80
+ return this.#message;
81
+ }
82
+
83
+ toString(): string {
84
+ return this.message;
85
+ }
86
+ }
87
+
88
+ export type LoadStatus = "ok" | "error" | "not-found";
89
+
90
+ export type LoadResult<T> =
91
+ | { value?: null; error: ConfigError; status: "error" }
92
+ | { value: T; error?: undefined; status: "ok" }
93
+ | { value?: null; error?: unknown; status: "not-found" };
94
+
95
+ export class ConfigFile<T> implements IConfigFile<T> {
96
+ readonly #basePath: string;
97
+ #cache?: LoadResult<T>;
98
+ #auxValidate?: (value: T) => void;
99
+
100
+ constructor(
101
+ readonly id: string,
102
+ readonly schema: TSchema,
103
+ configPath: string = path.join(getAgentDir(), `${id}.yml`),
104
+ ) {
105
+ this.#basePath = configPath;
106
+ if (configPath.endsWith(".yml")) {
107
+ const jsonPath = `${configPath.slice(0, -4)}.json`;
108
+ migrateJsonToYml(jsonPath, configPath);
109
+ } else if (configPath.endsWith(".yaml")) {
110
+ const jsonPath = `${configPath.slice(0, -5)}.json`;
111
+ migrateJsonToYml(jsonPath, configPath);
112
+ } else if (configPath.endsWith(".json") || configPath.endsWith(".jsonc")) {
113
+ // JSON configs are still supported without migration.
114
+ } else {
115
+ throw new Error(`Invalid config file path: ${configPath}`);
116
+ }
117
+ }
118
+
119
+ relocate(configPath?: string): ConfigFile<T> {
120
+ if (!configPath || configPath === this.#basePath) return this;
121
+ const result = new ConfigFile<T>(this.id, this.schema, configPath);
122
+ result.#auxValidate = this.#auxValidate;
123
+ return result;
124
+ }
125
+
126
+ getMtimeMs(): number | null {
127
+ try {
128
+ return fs.statSync(this.path()).mtimeMs;
129
+ } catch (err) {
130
+ if (isEnoent(err)) return null;
131
+ throw err;
132
+ }
133
+ }
134
+
135
+ withValidation(name: string, validate: (value: T) => void): this {
136
+ const prev = this.#auxValidate;
137
+ this.#auxValidate = (value: T) => {
138
+ prev?.(value);
139
+ try {
140
+ validate(value);
141
+ } catch (error) {
142
+ throw new ConfigError(this.id, undefined, { err: error, stage: `Validate(${name})` });
143
+ }
144
+ };
145
+ return this;
146
+ }
147
+
148
+ createDefault(): T {
149
+ return Value.Default(this.schema, [], undefined) as T;
150
+ }
151
+
152
+ #storeCache(result: LoadResult<T>): LoadResult<T> {
153
+ this.#cache = result;
154
+ return result;
155
+ }
156
+
157
+ tryLoad(): LoadResult<T> {
158
+ if (this.#cache) return this.#cache;
159
+
160
+ try {
161
+ const content = fs.readFileSync(this.path(), "utf-8").trim();
162
+
163
+ let parsed: unknown;
164
+ if (this.#basePath.endsWith(".json") || this.#basePath.endsWith(".jsonc")) {
165
+ parsed = JSONC.parse(content);
166
+ } else if (this.#basePath.endsWith(".yml") || this.#basePath.endsWith(".yaml")) {
167
+ parsed = YAML.parse(content);
168
+ } else {
169
+ throw new Error(`Invalid config file path: ${this.#basePath}`);
170
+ }
171
+
172
+ if (!Value.Check(this.schema, parsed)) {
173
+ const schemaErrors: ErrorObject[] = [];
174
+ for (const err of Value.Errors(this.schema, parsed)) {
175
+ schemaErrors.push({ instancePath: err.path, message: err.message } as ErrorObject);
176
+ if (schemaErrors.length >= 50) break;
177
+ }
178
+ const error = new ConfigError(this.id, schemaErrors);
179
+ logger.warn("Failed to parse config file", { path: this.path(), error });
180
+ return this.#storeCache({ error, status: "error" });
181
+ }
182
+ return this.#storeCache({ value: parsed as T, status: "ok" });
183
+ } catch (error) {
184
+ if (isEnoent(error)) {
185
+ return this.#storeCache({ status: "not-found" });
186
+ }
187
+ logger.warn("Failed to parse config file", { path: this.path(), error });
188
+ return this.#storeCache({
189
+ error: new ConfigError(this.id, undefined, { err: error, stage: "Unexpected" }),
190
+ status: "error",
191
+ });
192
+ }
193
+ }
194
+
195
+ load(): T | null {
196
+ return this.tryLoad().value ?? null;
197
+ }
198
+
199
+ loadOrDefault(): T {
200
+ return this.tryLoad().value ?? this.createDefault();
201
+ }
202
+
203
+ path(): string {
204
+ return this.#basePath;
205
+ }
206
+
207
+ invalidate() {
208
+ this.#cache = undefined;
209
+ }
210
+ }
@@ -72,15 +72,12 @@ const TRAILING_MARKER_SUFFIXES: readonly string[] = (() => {
72
72
  })();
73
73
  const WRAPPER_PREFIXES = ["duo-chat-"] as const;
74
74
 
75
- let __referenceDataCache: CanonicalReferenceData | undefined;
75
+ let referenceDataCache: CanonicalReferenceData | undefined;
76
76
  const EMPTY_COMPILED_EQUIVALENCE: CompiledEquivalenceConfig = {
77
77
  overrides: new Map<string, string>(),
78
78
  exclude: new Set<string>(),
79
79
  };
80
- const __resolutionCache: WeakMap<
81
- CompiledEquivalenceConfig,
82
- WeakMap<Model<Api>, ResolvedCanonicalModel>
83
- > = new WeakMap();
80
+ const resolutionCache: WeakMap<CompiledEquivalenceConfig, WeakMap<Model<Api>, ResolvedCanonicalModel>> = new WeakMap();
84
81
  const FAMILY_EXTRACTION_PATTERNS = [
85
82
  /(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+)(?::|$)/i,
86
83
  /(?:^|[/:._-])((?:claude|gemini|gpt|grok|glm|qwen|minimax|kimi|deepseek|llama|gemma|nova|mistral|ministral|pixtral|codestral|devstral|magistral|ernie|doubao|seed|aion|olmo|molmo|nemotron|palmyra|command|codex|coder|o[1345])[-a-z0-9.]+(?:[-_/][a-z0-9.]+)*)(?::|$)/i,
@@ -98,8 +95,8 @@ function shouldReplaceReference(existing: Model<Api> | undefined, candidate: Mod
98
95
  }
99
96
 
100
97
  function createCanonicalReferenceData(): CanonicalReferenceData {
101
- if (__referenceDataCache) {
102
- return __referenceDataCache;
98
+ if (referenceDataCache) {
99
+ return referenceDataCache;
103
100
  }
104
101
  const references = new Map<string, Model<Api>>();
105
102
  for (const provider of getBundledProviders()) {
@@ -112,11 +109,11 @@ function createCanonicalReferenceData(): CanonicalReferenceData {
112
109
  }
113
110
  }
114
111
  const officialIds = new Set(references.keys());
115
- __referenceDataCache = {
112
+ referenceDataCache = {
116
113
  references: Object.freeze(references) as Map<string, Model<Api>>,
117
114
  officialIds: Object.freeze(officialIds) as Set<string>,
118
115
  };
119
- return __referenceDataCache;
116
+ return referenceDataCache;
120
117
  }
121
118
 
122
119
  function normalizeSelectorKey(selector: string): string {
@@ -668,10 +665,10 @@ export function buildCanonicalModelIndex(
668
665
  const byId = new Map<string, CanonicalModelRecord>();
669
666
  const bySelector = new Map<string, string>();
670
667
 
671
- let modelCache = __resolutionCache.get(compiledEquivalence);
668
+ let modelCache = resolutionCache.get(compiledEquivalence);
672
669
  if (!modelCache) {
673
670
  modelCache = new WeakMap<Model<Api>, ResolvedCanonicalModel>();
674
- __resolutionCache.set(compiledEquivalence, modelCache);
671
+ resolutionCache.set(compiledEquivalence, modelCache);
675
672
  }
676
673
 
677
674
  for (const model of models) {
@@ -18,6 +18,8 @@ import {
18
18
  registerCustomApi,
19
19
  type SimpleStreamOptions,
20
20
  type ThinkingConfig,
21
+ UNK_CONTEXT_WINDOW,
22
+ UNK_MAX_TOKENS,
21
23
  unregisterCustomApis,
22
24
  } from "@oh-my-pi/pi-ai";
23
25
 
@@ -29,10 +31,10 @@ import { registerOAuthProvider, unregisterOAuthProviders } from "@oh-my-pi/pi-ai
29
31
  import type { OAuthCredentials, OAuthLoginCallbacks } from "@oh-my-pi/pi-ai/utils/oauth/types";
30
32
  import { isRecord, logger } from "@oh-my-pi/pi-utils";
31
33
  import { type Static, Type } from "@sinclair/typebox";
32
- import { type ConfigError, ConfigFile } from "../config";
33
34
  import { parseModelString, resolveProviderModelReference } from "../config/model-resolver";
34
35
  import { isValidThemeColor, type ThemeColor } from "../modes/theme/theme";
35
36
  import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
37
+ import { type ConfigError, ConfigFile } from "./config-file";
36
38
  import {
37
39
  buildCanonicalModelIndex,
38
40
  type CanonicalModelIndex,
@@ -1053,7 +1055,16 @@ export class ModelRegistry {
1053
1055
  const key = `${replacementModel.provider}\u0000${replacementModel.id}`;
1054
1056
  const existingIndex = indexByKey.get(key);
1055
1057
  if (existingIndex !== undefined) {
1056
- merged[existingIndex] = replacementModel;
1058
+ const existing = merged[existingIndex];
1059
+ merged[existingIndex] = {
1060
+ ...replacementModel,
1061
+ contextWindow:
1062
+ replacementModel.contextWindow === UNK_CONTEXT_WINDOW
1063
+ ? existing.contextWindow
1064
+ : replacementModel.contextWindow,
1065
+ maxTokens:
1066
+ replacementModel.maxTokens === UNK_MAX_TOKENS ? existing.maxTokens : replacementModel.maxTokens,
1067
+ };
1057
1068
  } else {
1058
1069
  merged.push(replacementModel);
1059
1070
  indexByKey.set(key, merged.length - 1);
@@ -12,10 +12,10 @@ import {
12
12
  type Model,
13
13
  modelsAreEqual,
14
14
  } from "@oh-my-pi/pi-ai";
15
+ import { fuzzyMatch } from "@oh-my-pi/pi-tui";
15
16
  import chalk from "chalk";
16
17
  import MODEL_PRIO from "../priority.json" with { type: "json" };
17
18
  import { parseThinkingLevel, resolveThinkingLevelForModel } from "../thinking";
18
- import { fuzzyMatch } from "../utils/fuzzy";
19
19
  import { isAuthenticated, kNoAuth, MODEL_ROLE_IDS, type ModelRegistry, type ModelRole } from "./model-registry";
20
20
  import type { Settings } from "./settings";
21
21
 
@@ -607,9 +607,6 @@ export function resolveModelRoleValue(
607
607
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
608
608
  }
609
609
 
610
- const lastColonIndex = normalized.lastIndexOf(":");
611
- const _thinkingSelector =
612
- lastColonIndex > PREFIX_MODEL_ROLE.length ? parseThinkingLevel(normalized.slice(lastColonIndex + 1)) : undefined;
613
610
  const effectivePatterns = resolveConfiguredRolePattern(normalized, options?.settings);
614
611
  if (!effectivePatterns || effectivePatterns.length === 0) {
615
612
  return { model: undefined, thinkingLevel: undefined, explicitThinkingLevel: false, warning: undefined };
@@ -956,6 +953,36 @@ export async function resolveModelScope(
956
953
  return scopedModels;
957
954
  }
958
955
 
956
+ /**
957
+ * Resolve the set of models a session is allowed to use, given the active
958
+ * settings. Starts from `modelRegistry.getAvailable()` (so disabled providers
959
+ * and providers without credentials are already filtered out) and, when
960
+ * `enabledModels` is configured for the current path scope, further restricts
961
+ * the result to models matching those patterns.
962
+ *
963
+ * Returns the unfiltered available list when `enabledModels` is empty.
964
+ * Returns an empty list when `enabledModels` is configured but no available
965
+ * model matches any pattern — callers MUST treat this as "no usable model"
966
+ * rather than falling back to the global default (see issue #1022).
967
+ */
968
+ export async function resolveAllowedModels(
969
+ modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">,
970
+ settings: Settings | undefined,
971
+ preferences?: ModelMatchPreferences,
972
+ ): Promise<Model<Api>[]> {
973
+ const available = modelRegistry.getAvailable();
974
+ const patterns = settings?.get("enabledModels");
975
+ if (!patterns || patterns.length === 0) {
976
+ return available;
977
+ }
978
+ const scoped = await resolveModelScope(patterns, modelRegistry, preferences);
979
+ if (scoped.length === 0) {
980
+ return [];
981
+ }
982
+ const allowed = new Set(scoped.map(entry => `${entry.model.provider}/${entry.model.id}`));
983
+ return available.filter(model => allowed.has(`${model.provider}/${model.id}`));
984
+ }
985
+
959
986
  export interface ResolveCliModelResult {
960
987
  model: Model<Api> | undefined;
961
988
  selector?: string;