@oh-my-pi/pi-coding-agent 8.0.16 → 8.1.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.
Files changed (166) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/package.json +14 -11
  3. package/scripts/generate-wasm-b64.ts +24 -0
  4. package/src/capability/context-file.ts +1 -1
  5. package/src/capability/extension-module.ts +1 -1
  6. package/src/capability/extension.ts +1 -1
  7. package/src/capability/hook.ts +1 -1
  8. package/src/capability/instruction.ts +1 -1
  9. package/src/capability/mcp.ts +1 -1
  10. package/src/capability/prompt.ts +1 -1
  11. package/src/capability/rule.ts +1 -1
  12. package/src/capability/settings.ts +1 -1
  13. package/src/capability/skill.ts +1 -1
  14. package/src/capability/slash-command.ts +1 -1
  15. package/src/capability/ssh.ts +1 -1
  16. package/src/capability/system-prompt.ts +1 -1
  17. package/src/capability/tool.ts +1 -1
  18. package/src/cli/args.ts +1 -1
  19. package/src/cli/plugin-cli.ts +1 -5
  20. package/src/commit/agentic/agent.ts +309 -0
  21. package/src/commit/agentic/fallback.ts +96 -0
  22. package/src/commit/agentic/index.ts +359 -0
  23. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  24. package/src/commit/agentic/prompts/session-user.md +26 -0
  25. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  26. package/src/commit/agentic/prompts/system.md +40 -0
  27. package/src/commit/agentic/state.ts +74 -0
  28. package/src/commit/agentic/tools/analyze-file.ts +131 -0
  29. package/src/commit/agentic/tools/git-file-diff.ts +194 -0
  30. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  31. package/src/commit/agentic/tools/git-overview.ts +84 -0
  32. package/src/commit/agentic/tools/index.ts +56 -0
  33. package/src/commit/agentic/tools/propose-changelog.ts +128 -0
  34. package/src/commit/agentic/tools/propose-commit.ts +154 -0
  35. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  36. package/src/commit/agentic/tools/split-commit.ts +284 -0
  37. package/src/commit/agentic/topo-sort.ts +44 -0
  38. package/src/commit/agentic/trivial.ts +51 -0
  39. package/src/commit/agentic/validation.ts +200 -0
  40. package/src/commit/analysis/conventional.ts +169 -0
  41. package/src/commit/analysis/index.ts +4 -0
  42. package/src/commit/analysis/scope.ts +242 -0
  43. package/src/commit/analysis/summary.ts +114 -0
  44. package/src/commit/analysis/validation.ts +66 -0
  45. package/src/commit/changelog/detect.ts +36 -0
  46. package/src/commit/changelog/generate.ts +112 -0
  47. package/src/commit/changelog/index.ts +233 -0
  48. package/src/commit/changelog/parse.ts +44 -0
  49. package/src/commit/cli.ts +93 -0
  50. package/src/commit/git/diff.ts +148 -0
  51. package/src/commit/git/errors.ts +11 -0
  52. package/src/commit/git/index.ts +217 -0
  53. package/src/commit/git/operations.ts +53 -0
  54. package/src/commit/index.ts +5 -0
  55. package/src/commit/map-reduce/.map-phase.ts.kate-swp +0 -0
  56. package/src/commit/map-reduce/index.ts +63 -0
  57. package/src/commit/map-reduce/map-phase.ts +193 -0
  58. package/src/commit/map-reduce/reduce-phase.ts +147 -0
  59. package/src/commit/map-reduce/utils.ts +9 -0
  60. package/src/commit/message.ts +11 -0
  61. package/src/commit/model-selection.ts +84 -0
  62. package/src/commit/pipeline.ts +242 -0
  63. package/src/commit/prompts/analysis-system.md +155 -0
  64. package/src/commit/prompts/analysis-user.md +41 -0
  65. package/src/commit/prompts/changelog-system.md +56 -0
  66. package/src/commit/prompts/changelog-user.md +19 -0
  67. package/src/commit/prompts/file-observer-system.md +26 -0
  68. package/src/commit/prompts/file-observer-user.md +9 -0
  69. package/src/commit/prompts/reduce-system.md +60 -0
  70. package/src/commit/prompts/reduce-user.md +17 -0
  71. package/src/commit/prompts/summary-retry.md +4 -0
  72. package/src/commit/prompts/summary-system.md +52 -0
  73. package/src/commit/prompts/summary-user.md +13 -0
  74. package/src/commit/prompts/types-description.md +2 -0
  75. package/src/commit/types.ts +109 -0
  76. package/src/commit/utils/exclusions.ts +42 -0
  77. package/src/config/file-lock.ts +111 -0
  78. package/src/config/model-registry.ts +16 -7
  79. package/src/config/settings-manager.ts +115 -40
  80. package/src/config.ts +5 -5
  81. package/src/discovery/agents-md.ts +1 -1
  82. package/src/discovery/builtin.ts +1 -1
  83. package/src/discovery/claude.ts +1 -1
  84. package/src/discovery/cline.ts +1 -1
  85. package/src/discovery/codex.ts +1 -1
  86. package/src/discovery/cursor.ts +1 -1
  87. package/src/discovery/gemini.ts +1 -1
  88. package/src/discovery/github.ts +1 -1
  89. package/src/discovery/index.ts +11 -11
  90. package/src/discovery/mcp-json.ts +1 -1
  91. package/src/discovery/ssh.ts +1 -1
  92. package/src/discovery/vscode.ts +1 -1
  93. package/src/discovery/windsurf.ts +1 -1
  94. package/src/extensibility/custom-commands/loader.ts +1 -1
  95. package/src/extensibility/custom-commands/types.ts +1 -1
  96. package/src/extensibility/custom-tools/loader.ts +1 -1
  97. package/src/extensibility/custom-tools/types.ts +1 -1
  98. package/src/extensibility/extensions/loader.ts +1 -1
  99. package/src/extensibility/extensions/types.ts +1 -1
  100. package/src/extensibility/hooks/loader.ts +1 -1
  101. package/src/extensibility/hooks/types.ts +3 -3
  102. package/src/index.ts +10 -10
  103. package/src/ipy/executor.ts +97 -1
  104. package/src/lsp/index.ts +1 -1
  105. package/src/lsp/render.ts +90 -46
  106. package/src/main.ts +16 -3
  107. package/src/mcp/loader.ts +3 -3
  108. package/src/migrations.ts +3 -3
  109. package/src/modes/components/assistant-message.ts +29 -1
  110. package/src/modes/components/tool-execution.ts +5 -3
  111. package/src/modes/components/tree-selector.ts +1 -1
  112. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  113. package/src/modes/controllers/selector-controller.ts +1 -1
  114. package/src/modes/interactive-mode.ts +5 -3
  115. package/src/modes/rpc/rpc-client.ts +1 -1
  116. package/src/modes/rpc/rpc-mode.ts +1 -4
  117. package/src/modes/rpc/rpc-types.ts +1 -1
  118. package/src/modes/theme/mermaid-cache.ts +89 -0
  119. package/src/modes/theme/theme.ts +2 -0
  120. package/src/modes/types.ts +2 -2
  121. package/src/patch/index.ts +3 -9
  122. package/src/patch/shared.ts +33 -5
  123. package/src/prompts/tools/task.md +2 -0
  124. package/src/sdk.ts +60 -22
  125. package/src/session/agent-session.ts +3 -3
  126. package/src/session/agent-storage.ts +32 -28
  127. package/src/session/artifacts.ts +24 -1
  128. package/src/session/auth-storage.ts +25 -10
  129. package/src/session/storage-migration.ts +12 -53
  130. package/src/system-prompt.ts +2 -2
  131. package/src/task/.executor.ts.kate-swp +0 -0
  132. package/src/task/executor.ts +1 -1
  133. package/src/task/index.ts +10 -1
  134. package/src/task/output-manager.ts +94 -0
  135. package/src/task/render.ts +7 -12
  136. package/src/task/worker.ts +1 -1
  137. package/src/tools/ask.ts +35 -13
  138. package/src/tools/bash.ts +80 -87
  139. package/src/tools/calculator.ts +42 -40
  140. package/src/tools/complete.ts +1 -1
  141. package/src/tools/fetch.ts +67 -104
  142. package/src/tools/find.ts +83 -86
  143. package/src/tools/grep.ts +80 -96
  144. package/src/tools/index.ts +10 -7
  145. package/src/tools/ls.ts +39 -65
  146. package/src/tools/notebook.ts +48 -64
  147. package/src/tools/output-utils.ts +1 -1
  148. package/src/tools/python.ts +71 -183
  149. package/src/tools/read.ts +74 -15
  150. package/src/tools/render-utils.ts +1 -15
  151. package/src/tools/ssh.ts +43 -24
  152. package/src/tools/todo-write.ts +27 -15
  153. package/src/tools/write.ts +93 -64
  154. package/src/tui/code-cell.ts +115 -0
  155. package/src/tui/file-list.ts +48 -0
  156. package/src/tui/index.ts +11 -0
  157. package/src/tui/output-block.ts +73 -0
  158. package/src/tui/status-line.ts +40 -0
  159. package/src/tui/tree-list.ts +56 -0
  160. package/src/tui/types.ts +17 -0
  161. package/src/tui/utils.ts +49 -0
  162. package/src/vendor/photon/photon_rs_bg.wasm.b64.js +1 -0
  163. package/src/web/search/auth.ts +1 -1
  164. package/src/web/search/index.ts +1 -1
  165. package/src/web/search/render.ts +119 -163
  166. package/tsconfig.json +0 -42
@@ -0,0 +1,56 @@
1
+ You are an expert changelog writer who analyzes git diffs and produces Keep a Changelog entries. Get this right—changelogs are how users understand what changed.
2
+
3
+ <instructions>
4
+ Analyze the diff and return JSON changelog entries.
5
+
6
+ 1. Identify user-visible changes only
7
+ 2. Categorize each change (Added, Changed, Deprecated, Removed, Fixed, Security, Breaking Changes)
8
+ 3. Write entries starting with past-tense verb describing user impact
9
+ 4. Omit categories with no entries
10
+ 5. Return empty entries object for internal-only changes
11
+
12
+ This matters. Be thorough but precise.
13
+ </instructions>
14
+
15
+ <categories>
16
+ - Added: New features, public APIs, user-facing capabilities
17
+ - Changed: Modified existing behavior
18
+ - Deprecated: Features scheduled for removal
19
+ - Removed: Deleted features or APIs
20
+ - Fixed: Bug corrections with observable impact
21
+ - Security: Vulnerability fixes
22
+ - Breaking Changes: API-incompatible modifications (use sparingly)
23
+ </categories>
24
+
25
+ <entry_format>
26
+ - Start with past-tense verb (Added, Fixed, Implemented, Updated)
27
+ - Describe user-visible impact, not implementation
28
+ - Name the specific feature, option, or behavior
29
+ - Keep to 1-2 lines, no trailing periods
30
+ </entry_format>
31
+
32
+ <examples>
33
+ Good:
34
+ - Added --dry-run flag to preview changes without applying them
35
+ - Fixed memory leak when processing large files
36
+ - Changed default timeout from 30s to 60s for slow connections
37
+
38
+ Bad:
39
+ - **cli**: Added dry-run flag -> scope prefix redundant
40
+ - Added new feature. -> vague, has trailing period
41
+ - Refactored parser internals -> not user-visible
42
+
43
+ Breaking Changes example:
44
+ - Removed legacy auth flow; users must re-authenticate with OAuth tokens
45
+ </examples>
46
+
47
+ <exclude>
48
+ Internal refactoring, code style changes, test-only modifications, minor doc updates, anything invisible to users.
49
+ </exclude>
50
+
51
+ <output_format>
52
+ Return ONLY valid JSON. No markdown fences, no explanation.
53
+
54
+ With entries: {"entries": {"Added": ["entry 1"], "Fixed": ["entry 2"]}}
55
+ No changelog-worthy changes: {"entries": {}}
56
+ </output_format>
@@ -0,0 +1,19 @@
1
+ <context>
2
+ Changelog: {{ changelog_path }}
3
+ {{#if is_package_changelog}}Scope: Package-level changelog. Omit package name prefix from entries.{{/if}}
4
+ </context>
5
+ {{#if existing_entries}}
6
+
7
+ <existing_entries>
8
+ Already documented—skip these:
9
+ {{ existing_entries }}
10
+ </existing_entries>
11
+ {{/if}}
12
+
13
+ <diff_summary>
14
+ {{ stat }}
15
+ </diff_summary>
16
+
17
+ <diff>
18
+ {{ diff }}
19
+ </diff>
@@ -0,0 +1,26 @@
1
+ <role>Expert code analyst extracting structured observations from diffs.</role>
2
+
3
+ <instructions>
4
+ Extract factual observations from the diff. This matters—be precise.
5
+
6
+ 1. Use past-tense verb + specific target + optional purpose
7
+ 2. Max 100 characters per observation
8
+ 3. Consolidate related changes (e.g., "renamed 5 helper functions")
9
+ 4. Return 1-5 observations only
10
+ </instructions>
11
+
12
+ <scope>
13
+ Include: functions, methods, types, API changes, behavior/logic changes, error handling, performance, security.
14
+
15
+ Exclude: import reordering, whitespace/formatting, comment-only changes, debug statements.
16
+ </scope>
17
+
18
+ <output_format>
19
+ Plain list, no preamble, no summary, no markdown formatting.
20
+
21
+ - added 'parse_config()' function for TOML configuration loading
22
+ - removed deprecated 'legacy_init()' and all callers
23
+ - changed 'Connection::new()' to accept '&Config' instead of individual params
24
+ </output_format>
25
+
26
+ Observations only. Classification happens in reduce phase.
@@ -0,0 +1,9 @@
1
+ <file path="{{ filename }}">
2
+ {{ diff }}
3
+ </file>
4
+ {{#if context_header}}
5
+
6
+ <related_files>
7
+ {{ context_header }}
8
+ </related_files>
9
+ {{/if}}
@@ -0,0 +1,60 @@
1
+ You are a senior engineer synthesizing file-level observations into a conventional commit analysis.
2
+
3
+ <context>
4
+ Given map-phase observations from analyzed files, produce a unified commit classification with changelog metadata.
5
+ </context>
6
+
7
+ <instructions>
8
+ Determine:
9
+ 1. TYPE: Single classification for entire commit
10
+ 2. SCOPE: Primary component (null if multi-component)
11
+ 3. DETAILS: 3-4 summary points (max 6)
12
+ 4. CHANGELOG: Metadata for user-visible changes
13
+
14
+ Get this right. Accuracy matters.
15
+ </instructions>
16
+
17
+ <scope_rules>
18
+ - Use component name if >=60% of changes target it
19
+ - Use null if spread across multiple components
20
+ - Use scope_candidates as primary source
21
+ - Valid scopes only: specific component names (api, parser, config, etc.)
22
+ </scope_rules>
23
+
24
+ <output_format>
25
+ Each detail point:
26
+ - Past-tense verb start (added, fixed, moved, extracted)
27
+ - Under 120 characters, ends with period
28
+ - Group related cross-file changes
29
+
30
+ Priority: user-visible behavior > performance/security > architecture > internal implementation
31
+
32
+ changelog_category: Added | Changed | Fixed | Deprecated | Removed | Security
33
+ user_visible: true for features, user-facing bugs, breaking changes, security fixes
34
+ </output_format>
35
+
36
+ <example>
37
+ Input observations:
38
+ - api/client.ts: added token refresh guard to prevent duplicate refreshes
39
+ - api/http.ts: introduced retry wrapper for 429 responses
40
+ - api/index.ts: updated exports for retry helper
41
+
42
+ Output:
43
+ {
44
+ "type": "fix",
45
+ "scope": "api",
46
+ "details": [
47
+ {
48
+ "text": "Added token refresh guard to prevent duplicate refreshes.",
49
+ "changelog_category": "Fixed",
50
+ "user_visible": true
51
+ },
52
+ {
53
+ "text": "Introduced retry wrapper for 429 responses.",
54
+ "changelog_category": "Fixed",
55
+ "user_visible": true
56
+ }
57
+ ],
58
+ "issue_refs": []
59
+ }
60
+ </example>
@@ -0,0 +1,17 @@
1
+ {{#if types_description}}
2
+ <type_definitions>
3
+ {{ types_description }}
4
+ </type_definitions>
5
+ {{/if}}
6
+
7
+ <observations>
8
+ {{ observations }}
9
+ </observations>
10
+
11
+ <diff_statistics>
12
+ {{ stat }}
13
+ </diff_statistics>
14
+
15
+ <scope_candidates>
16
+ {{ scope_candidates }}
17
+ </scope_candidates>
@@ -0,0 +1,4 @@
1
+ {{#if base_context}}
2
+ {{ base_context }}
3
+
4
+ {{/if}}Previous summary failed validation: {{ errors }}
@@ -0,0 +1,52 @@
1
+ You are a commit message specialist generating precise, informative descriptions.
2
+
3
+ <context>
4
+ Output: ONLY the description after "{{ commit_type }}{{ scope_prefix }}:".
5
+ Constraint: {{ chars }} characters max, no trailing period, no type prefix in output.
6
+ </context>
7
+
8
+ <instructions>
9
+ 1. Start with lowercase past-tense verb (must differ from "{{ commit_type }}")
10
+ 2. Name the specific subsystem/component affected
11
+ 3. Include WHY when it clarifies intent
12
+ 4. One focused concept per message
13
+
14
+ Get this right.
15
+ </instructions>
16
+
17
+ <verb_reference>
18
+ | Type | Use instead |
19
+ |----------|-------------------------------------------------|
20
+ | feat | added, introduced, implemented, enabled |
21
+ | fix | corrected, resolved, patched, addressed |
22
+ | refactor | restructured, reorganized, migrated, simplified |
23
+ | perf | optimized, reduced, eliminated, accelerated |
24
+ | docs | documented, clarified, expanded |
25
+ | build | upgraded, pinned, configured |
26
+ | chore | cleaned, removed, renamed, organized |
27
+ </verb_reference>
28
+
29
+ <examples>
30
+ feat | TLS encryption added to HTTP client for MITM prevention
31
+ -> added TLS support to prevent man-in-the-middle attacks
32
+
33
+ refactor | Consolidated HTTP transport into unified builder pattern
34
+ -> migrated HTTP transport to unified builder API
35
+
36
+ fix | Race condition in connection pool causing exhaustion under load
37
+ -> corrected race condition causing connection pool exhaustion
38
+
39
+ perf | Batch processing optimized to reduce memory allocations
40
+ -> eliminated allocation overhead in batch processing
41
+
42
+ build | Updated serde to fix CVE-2024-1234
43
+ -> upgraded serde to 1.0.200 for CVE-2024-1234
44
+ </examples>
45
+
46
+ <banned_words>
47
+ comprehensive, various, several, improved, enhanced, quickly, simply, basically, this change, this commit, now
48
+ </banned_words>
49
+
50
+ <output_format>
51
+ Output the description text only. Include motivation, name specifics, stay focused.
52
+ </output_format>
@@ -0,0 +1,13 @@
1
+ {{#if user_context}}
2
+ <user_context>
3
+ {{ user_context }}
4
+ </user_context>
5
+ {{/if}}
6
+
7
+ <detail_points>
8
+ {{ details }}
9
+ </detail_points>
10
+
11
+ <diff_stat>
12
+ {{ stat }}
13
+ </diff_stat>
@@ -0,0 +1,2 @@
1
+ Types: feat, fix, refactor, perf, docs, test, build, ci, chore, style, revert.
2
+ Format: <type>(<scope>): <summary> with past-tense summary.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Types for the omp commit pipeline.
3
+ */
4
+
5
+ export type CommitType =
6
+ | "feat"
7
+ | "fix"
8
+ | "refactor"
9
+ | "perf"
10
+ | "docs"
11
+ | "test"
12
+ | "build"
13
+ | "ci"
14
+ | "chore"
15
+ | "style"
16
+ | "revert";
17
+
18
+ export type ChangelogCategory =
19
+ | "Breaking Changes"
20
+ | "Added"
21
+ | "Changed"
22
+ | "Deprecated"
23
+ | "Removed"
24
+ | "Fixed"
25
+ | "Security";
26
+
27
+ export interface CommitCommandArgs {
28
+ /** Push after commit */
29
+ push: boolean;
30
+ /** Preview without committing */
31
+ dryRun: boolean;
32
+ /** Skip changelog updates */
33
+ noChangelog: boolean;
34
+ /** Use legacy deterministic pipeline */
35
+ legacy?: boolean;
36
+ /** Additional user context for the model */
37
+ context?: string;
38
+ /** Override the model selection */
39
+ model?: string;
40
+ }
41
+
42
+ export interface NumstatEntry {
43
+ path: string;
44
+ additions: number;
45
+ deletions: number;
46
+ }
47
+
48
+ export interface ConventionalDetail {
49
+ text: string;
50
+ changelogCategory?: ChangelogCategory;
51
+ userVisible: boolean;
52
+ }
53
+
54
+ export interface ConventionalAnalysis {
55
+ type: CommitType;
56
+ scope: string | null;
57
+ details: ConventionalDetail[];
58
+ issueRefs: string[];
59
+ }
60
+
61
+ export interface CommitSummary {
62
+ summary: string;
63
+ }
64
+
65
+ export interface FileObservation {
66
+ file: string;
67
+ observations: string[];
68
+ additions: number;
69
+ deletions: number;
70
+ }
71
+
72
+ export interface FileDiff {
73
+ filename: string;
74
+ content: string;
75
+ additions: number;
76
+ deletions: number;
77
+ isBinary: boolean;
78
+ }
79
+
80
+ export interface DiffHunk {
81
+ index: number;
82
+ header: string;
83
+ oldStart: number;
84
+ oldLines: number;
85
+ newStart: number;
86
+ newLines: number;
87
+ content: string;
88
+ }
89
+
90
+ export interface FileHunks {
91
+ filename: string;
92
+ isBinary: boolean;
93
+ hunks: DiffHunk[];
94
+ }
95
+
96
+ export interface ChangelogBoundary {
97
+ changelogPath: string;
98
+ files: string[];
99
+ }
100
+
101
+ export interface UnreleasedSection {
102
+ startLine: number;
103
+ endLine: number;
104
+ entries: Record<string, string[]>;
105
+ }
106
+
107
+ export interface ChangelogGenerationResult {
108
+ entries: Record<string, string[]>;
109
+ }
@@ -0,0 +1,42 @@
1
+ const EXCLUDED_FILES = [
2
+ "Cargo.lock",
3
+ "package-lock.json",
4
+ "npm-shrinkwrap.json",
5
+ "yarn.lock",
6
+ "pnpm-lock.yaml",
7
+ "shrinkwrap.yaml",
8
+ "bun.lock",
9
+ "bun.lockb",
10
+ "deno.lock",
11
+ "composer.lock",
12
+ "Gemfile.lock",
13
+ "poetry.lock",
14
+ "Pipfile.lock",
15
+ "pdm.lock",
16
+ "uv.lock",
17
+ "go.sum",
18
+ "flake.lock",
19
+ "pubspec.lock",
20
+ "Podfile.lock",
21
+ "Packages.resolved",
22
+ "mix.lock",
23
+ "packages.lock.json",
24
+ "config.yml.lock",
25
+ "config.yaml.lock",
26
+ "settings.yml.lock",
27
+ "settings.yaml.lock",
28
+ ];
29
+
30
+ const EXCLUDED_SUFFIXES = [".lock.yml", ".lock.yaml", "-lock.yml", "-lock.yaml"];
31
+
32
+ export function isExcludedFile(path: string): boolean {
33
+ const lower = path.toLowerCase();
34
+ if (EXCLUDED_FILES.some((name) => lower.endsWith(name.toLowerCase()))) {
35
+ return true;
36
+ }
37
+ return EXCLUDED_SUFFIXES.some((suffix) => lower.endsWith(suffix));
38
+ }
39
+
40
+ export function filterExcludedFiles<T extends { filename: string }>(files: T[]): T[] {
41
+ return files.filter((file) => !isExcludedFile(file.filename));
42
+ }
@@ -0,0 +1,111 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, rm } from "node:fs/promises";
3
+
4
+ export interface FileLockOptions {
5
+ staleMs?: number;
6
+ retries?: number;
7
+ retryDelayMs?: number;
8
+ }
9
+
10
+ const DEFAULT_OPTIONS: Required<FileLockOptions> = {
11
+ staleMs: 10_000,
12
+ retries: 50,
13
+ retryDelayMs: 100,
14
+ };
15
+
16
+ interface LockInfo {
17
+ pid: number;
18
+ timestamp: number;
19
+ }
20
+
21
+ function getLockPath(filePath: string): string {
22
+ return `${filePath}.lock`;
23
+ }
24
+
25
+ async function writeLockInfo(lockPath: string): Promise<void> {
26
+ const info: LockInfo = { pid: process.pid, timestamp: Date.now() };
27
+ await Bun.write(`${lockPath}/info`, JSON.stringify(info));
28
+ }
29
+
30
+ async function readLockInfo(lockPath: string): Promise<LockInfo | null> {
31
+ try {
32
+ const content = await readFile(`${lockPath}/info`, "utf-8");
33
+ return JSON.parse(content) as LockInfo;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function isProcessAlive(pid: number): boolean {
40
+ try {
41
+ process.kill(pid, 0);
42
+ return true;
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ async function isLockStale(lockPath: string, staleMs: number): Promise<boolean> {
49
+ const info = await readLockInfo(lockPath);
50
+ if (!info) return true;
51
+
52
+ if (!isProcessAlive(info.pid)) return true;
53
+
54
+ if (Date.now() - info.timestamp > staleMs) return true;
55
+
56
+ return false;
57
+ }
58
+
59
+ async function tryAcquireLock(lockPath: string): Promise<boolean> {
60
+ try {
61
+ await mkdir(lockPath);
62
+ await writeLockInfo(lockPath);
63
+ return true;
64
+ } catch (error) {
65
+ if ((error as NodeJS.ErrnoException).code === "EEXIST") {
66
+ return false;
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ async function releaseLock(lockPath: string): Promise<void> {
73
+ try {
74
+ await rm(lockPath, { recursive: true });
75
+ } catch {
76
+ // Ignore errors on release
77
+ }
78
+ }
79
+
80
+ async function acquireLock(filePath: string, options: FileLockOptions = {}): Promise<() => Promise<void>> {
81
+ const opts = { ...DEFAULT_OPTIONS, ...options };
82
+ const lockPath = getLockPath(filePath);
83
+
84
+ for (let attempt = 0; attempt < opts.retries; attempt++) {
85
+ if (await tryAcquireLock(lockPath)) {
86
+ return () => releaseLock(lockPath);
87
+ }
88
+
89
+ if (existsSync(lockPath) && (await isLockStale(lockPath, opts.staleMs))) {
90
+ await releaseLock(lockPath);
91
+ continue;
92
+ }
93
+
94
+ await new Promise((resolve) => setTimeout(resolve, opts.retryDelayMs));
95
+ }
96
+
97
+ throw new Error(`Failed to acquire lock for ${filePath} after ${opts.retries} attempts`);
98
+ }
99
+
100
+ export async function withFileLock<T>(
101
+ filePath: string,
102
+ fn: () => Promise<T>,
103
+ options: FileLockOptions = {},
104
+ ): Promise<T> {
105
+ const release = await acquireLock(filePath, options);
106
+ try {
107
+ return await fn();
108
+ } finally {
109
+ await release();
110
+ }
111
+ }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { existsSync, readFileSync } from "node:fs";
6
+ import { extname } from "node:path";
6
7
  import {
7
8
  type Api,
8
9
  getGitHubCopilotBaseUrl,
@@ -15,6 +16,7 @@ import type { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage
15
16
  import { logger } from "@oh-my-pi/pi-utils";
16
17
  import { type Static, Type } from "@sinclair/typebox";
17
18
  import AjvModule from "ajv";
19
+ import { YAML } from "bun";
18
20
 
19
21
  const Ajv = (AjvModule as any).default || AjvModule;
20
22
 
@@ -262,14 +264,21 @@ export class ModelRegistry {
262
264
  });
263
265
  }
264
266
 
265
- private loadCustomModels(modelsJsonPath: string): CustomModelsResult {
266
- if (!existsSync(modelsJsonPath)) {
267
+ private loadCustomModels(modelsPath: string): CustomModelsResult {
268
+ if (!existsSync(modelsPath)) {
267
269
  return emptyCustomModelsResult();
268
270
  }
269
271
 
270
272
  try {
271
- const content = readFileSync(modelsJsonPath, "utf-8");
272
- const config: ModelsConfig = JSON.parse(content);
273
+ const content = readFileSync(modelsPath, "utf-8");
274
+ const ext = extname(modelsPath).toLowerCase();
275
+ let config: ModelsConfig;
276
+
277
+ if (ext === ".yaml" || ext === ".yml") {
278
+ config = YAML.parse(content) as ModelsConfig;
279
+ } else {
280
+ config = JSON.parse(content) as ModelsConfig;
281
+ }
273
282
 
274
283
  // Validate schema
275
284
  const ajv = new Ajv();
@@ -278,7 +287,7 @@ export class ModelRegistry {
278
287
  const errors =
279
288
  validate.errors?.map((e: any) => ` - ${e.instancePath || "root"}: ${e.message}`).join("\n") ||
280
289
  "Unknown schema error";
281
- return emptyCustomModelsResult(`Invalid models.json schema:\n${errors}\n\nFile: ${modelsJsonPath}`);
290
+ return emptyCustomModelsResult(`Invalid models config schema:\n${errors}\n\nFile: ${modelsPath}`);
282
291
  }
283
292
 
284
293
  // Additional validation
@@ -309,10 +318,10 @@ export class ModelRegistry {
309
318
  return { models: this.parseModels(config), replacedProviders, overrides, error: undefined };
310
319
  } catch (error) {
311
320
  if (error instanceof SyntaxError) {
312
- return emptyCustomModelsResult(`Failed to parse models.json: ${error.message}\n\nFile: ${modelsJsonPath}`);
321
+ return emptyCustomModelsResult(`Failed to parse models config: ${error.message}\n\nFile: ${modelsPath}`);
313
322
  }
314
323
  return emptyCustomModelsResult(
315
- `Failed to load models.json: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsJsonPath}`,
324
+ `Failed to load models config: ${error instanceof Error ? error.message : error}\n\nFile: ${modelsPath}`,
316
325
  );
317
326
  }
318
327
  }