@oh-my-pi/pi-coding-agent 8.0.20 → 8.2.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 +125 -0
- package/docs/session.md +111 -46
- package/examples/custom-tools/hello/index.ts +1 -1
- package/examples/custom-tools/todo/index.ts +3 -4
- package/examples/extensions/api-demo.ts +0 -1
- package/examples/extensions/chalk-logger.ts +2 -3
- package/examples/extensions/hello.ts +0 -1
- package/examples/extensions/pirate.ts +0 -1
- package/examples/extensions/plan-mode.ts +15 -16
- package/examples/extensions/todo.ts +3 -4
- package/examples/extensions/tools.ts +1 -2
- package/examples/extensions/with-deps/index.ts +0 -1
- package/examples/hooks/auto-commit-on-exit.ts +1 -2
- package/examples/hooks/confirm-destructive.ts +0 -1
- package/examples/hooks/custom-compaction.ts +1 -2
- package/examples/hooks/dirty-repo-guard.ts +0 -1
- package/examples/hooks/file-trigger.ts +3 -4
- package/examples/hooks/git-checkpoint.ts +0 -1
- package/examples/hooks/handoff.ts +3 -4
- package/examples/hooks/permission-gate.ts +1 -2
- package/examples/hooks/protected-paths.ts +1 -2
- package/examples/hooks/qna.ts +2 -3
- package/examples/hooks/snake.ts +4 -5
- package/examples/hooks/status-line.ts +0 -1
- package/examples/sdk/01-minimal.ts +2 -3
- package/examples/sdk/02-custom-model.ts +2 -3
- package/examples/sdk/03-custom-prompt.ts +3 -4
- package/examples/sdk/04-skills.ts +2 -3
- package/examples/sdk/06-extensions.ts +1 -2
- package/examples/sdk/06-hooks.ts +6 -7
- package/examples/sdk/07-context-files.ts +0 -1
- package/examples/sdk/08-prompt-templates.ts +0 -1
- package/examples/sdk/08-slash-commands.ts +0 -1
- package/examples/sdk/09-api-keys-and-oauth.ts +0 -1
- package/examples/sdk/10-settings.ts +0 -1
- package/examples/sdk/11-sessions.ts +0 -1
- package/package.json +54 -23
- package/scripts/format-prompts.ts +0 -1
- package/src/capability/context-file.ts +3 -4
- package/src/capability/extension-module.ts +3 -4
- package/src/capability/extension.ts +3 -4
- package/src/capability/fs.ts +20 -21
- package/src/capability/hook.ts +3 -4
- package/src/capability/index.ts +15 -16
- package/src/capability/instruction.ts +3 -4
- package/src/capability/mcp.ts +3 -4
- package/src/capability/prompt.ts +3 -4
- package/src/capability/rule.ts +3 -4
- package/src/capability/settings.ts +2 -3
- package/src/capability/skill.ts +3 -4
- package/src/capability/slash-command.ts +3 -4
- package/src/capability/ssh.ts +3 -4
- package/src/capability/system-prompt.ts +3 -4
- package/src/capability/tool.ts +3 -4
- package/src/cli/args.ts +5 -6
- package/src/cli/config-cli.ts +6 -7
- package/src/cli/file-processor.ts +19 -17
- package/src/cli/jupyter-cli.ts +105 -0
- package/src/cli/list-models.ts +10 -11
- package/src/cli/plugin-cli.ts +20 -25
- package/src/cli/session-picker.ts +2 -3
- package/src/cli/setup-cli.ts +2 -3
- package/src/cli/stats-cli.ts +2 -3
- package/src/cli/update-cli.ts +25 -22
- package/src/commit/agentic/agent.ts +307 -0
- package/src/commit/agentic/fallback.ts +96 -0
- package/src/commit/agentic/index.ts +351 -0
- package/src/commit/agentic/prompts/analyze-file.md +22 -0
- package/src/commit/agentic/prompts/session-user.md +26 -0
- package/src/commit/agentic/prompts/split-confirm.md +1 -0
- package/src/commit/agentic/prompts/system.md +40 -0
- package/src/commit/agentic/state.ts +69 -0
- package/src/commit/agentic/tools/analyze-file.ts +131 -0
- package/src/commit/agentic/tools/git-file-diff.ts +194 -0
- package/src/commit/agentic/tools/git-hunk.ts +50 -0
- package/src/commit/agentic/tools/git-overview.ts +84 -0
- package/src/commit/agentic/tools/index.ts +56 -0
- package/src/commit/agentic/tools/propose-changelog.ts +128 -0
- package/src/commit/agentic/tools/propose-commit.ts +154 -0
- package/src/commit/agentic/tools/recent-commits.ts +81 -0
- package/src/commit/agentic/tools/split-commit.ts +280 -0
- package/src/commit/agentic/topo-sort.ts +44 -0
- package/src/commit/agentic/trivial.ts +51 -0
- package/src/commit/agentic/validation.ts +200 -0
- package/src/commit/analysis/conventional.ts +165 -0
- package/src/commit/analysis/index.ts +4 -0
- package/src/commit/analysis/scope.ts +242 -0
- package/src/commit/analysis/summary.ts +112 -0
- package/src/commit/analysis/validation.ts +66 -0
- package/src/commit/changelog/detect.ts +36 -0
- package/src/commit/changelog/generate.ts +110 -0
- package/src/commit/changelog/index.ts +233 -0
- package/src/commit/changelog/parse.ts +44 -0
- package/src/commit/cli.ts +93 -0
- package/src/commit/git/diff.ts +148 -0
- package/src/commit/git/errors.ts +11 -0
- package/src/commit/git/index.ts +212 -0
- package/src/commit/git/operations.ts +53 -0
- package/src/commit/index.ts +5 -0
- package/src/commit/map-reduce/index.ts +63 -0
- package/src/commit/map-reduce/map-phase.ts +178 -0
- package/src/commit/map-reduce/reduce-phase.ts +145 -0
- package/src/commit/map-reduce/utils.ts +9 -0
- package/src/commit/message.ts +11 -0
- package/src/commit/model-selection.ts +80 -0
- package/src/commit/pipeline.ts +240 -0
- package/src/commit/prompts/analysis-system.md +155 -0
- package/src/commit/prompts/analysis-user.md +41 -0
- package/src/commit/prompts/changelog-system.md +56 -0
- package/src/commit/prompts/changelog-user.md +19 -0
- package/src/commit/prompts/file-observer-system.md +26 -0
- package/src/commit/prompts/file-observer-user.md +9 -0
- package/src/commit/prompts/reduce-system.md +60 -0
- package/src/commit/prompts/reduce-user.md +17 -0
- package/src/commit/prompts/summary-retry.md +4 -0
- package/src/commit/prompts/summary-system.md +52 -0
- package/src/commit/prompts/summary-user.md +13 -0
- package/src/commit/prompts/types-description.md +2 -0
- package/src/commit/types.ts +109 -0
- package/src/commit/utils/exclusions.ts +42 -0
- package/src/config/file-lock.ts +121 -0
- package/src/config/keybindings.ts +6 -8
- package/src/config/model-registry.ts +65 -38
- package/src/config/model-resolver.ts +18 -19
- package/src/config/prompt-templates.ts +11 -11
- package/src/config/settings-manager.ts +141 -50
- package/src/config.ts +64 -66
- package/src/cursor.ts +11 -9
- package/src/discovery/agents-md.ts +11 -12
- package/src/discovery/builtin.ts +68 -73
- package/src/discovery/claude.ts +41 -42
- package/src/discovery/cline.ts +11 -12
- package/src/discovery/codex.ts +52 -53
- package/src/discovery/cursor.ts +9 -10
- package/src/discovery/gemini.ts +17 -22
- package/src/discovery/github.ts +13 -14
- package/src/discovery/helpers.ts +35 -34
- package/src/discovery/index.ts +22 -24
- package/src/discovery/mcp-json.ts +8 -9
- package/src/discovery/ssh.ts +8 -9
- package/src/discovery/vscode.ts +4 -5
- package/src/discovery/windsurf.ts +6 -7
- package/src/exa/company.ts +1 -2
- package/src/exa/index.ts +2 -3
- package/src/exa/linkedin.ts +1 -2
- package/src/exa/mcp-client.ts +14 -16
- package/src/exa/render.ts +10 -11
- package/src/exa/researcher.ts +1 -2
- package/src/exa/search.ts +1 -2
- package/src/exa/types.ts +0 -1
- package/src/exa/websets.ts +1 -2
- package/src/exec/bash-executor.ts +3 -4
- package/src/exec/exec.ts +0 -1
- package/src/export/custom-share.ts +5 -6
- package/src/export/html/index.ts +24 -21
- package/src/export/ttsr.ts +2 -3
- package/src/extensibility/custom-commands/bundled/review/index.ts +7 -8
- package/src/extensibility/custom-commands/loader.ts +18 -15
- package/src/extensibility/custom-commands/types.ts +2 -3
- package/src/extensibility/custom-tools/loader.ts +11 -12
- package/src/extensibility/custom-tools/types.ts +7 -8
- package/src/extensibility/custom-tools/wrapper.ts +2 -3
- package/src/extensibility/extensions/loader.ts +76 -54
- package/src/extensibility/extensions/runner.ts +11 -12
- package/src/extensibility/extensions/types.ts +20 -27
- package/src/extensibility/extensions/wrapper.ts +3 -4
- package/src/extensibility/hooks/index.ts +1 -1
- package/src/extensibility/hooks/loader.ts +9 -10
- package/src/extensibility/hooks/runner.ts +7 -8
- package/src/extensibility/hooks/tool-wrapper.ts +0 -1
- package/src/extensibility/hooks/types.ts +11 -18
- package/src/extensibility/plugins/doctor.ts +3 -3
- package/src/extensibility/plugins/installer.ts +27 -27
- package/src/extensibility/plugins/loader.ts +59 -56
- package/src/extensibility/plugins/manager.ts +211 -171
- package/src/extensibility/plugins/parser.ts +1 -1
- package/src/extensibility/plugins/paths.ts +8 -8
- package/src/extensibility/skills.ts +63 -60
- package/src/extensibility/slash-commands.ts +10 -10
- package/src/index.ts +54 -54
- package/src/internal-urls/agent-protocol.ts +21 -11
- package/src/internal-urls/artifact-protocol.ts +17 -13
- package/src/internal-urls/router.ts +1 -2
- package/src/internal-urls/rule-protocol.ts +3 -4
- package/src/internal-urls/skill-protocol.ts +3 -4
- package/src/ipy/executor.ts +109 -9
- package/src/ipy/gateway-coordinator.ts +79 -90
- package/src/ipy/kernel.ts +32 -30
- package/src/ipy/modules.ts +13 -13
- package/src/lsp/client.ts +21 -10
- package/src/lsp/clients/biome-client.ts +1 -2
- package/src/lsp/clients/index.ts +3 -3
- package/src/lsp/clients/lsp-linter-client.ts +4 -5
- package/src/lsp/config.ts +15 -15
- package/src/lsp/edits.ts +4 -5
- package/src/lsp/index.ts +43 -44
- package/src/lsp/lspmux.ts +8 -8
- package/src/lsp/render.ts +99 -61
- package/src/lsp/utils.ts +3 -3
- package/src/main.ts +71 -37
- package/src/mcp/client.ts +2 -3
- package/src/mcp/config.ts +5 -6
- package/src/mcp/json-rpc.ts +0 -1
- package/src/mcp/loader.ts +6 -7
- package/src/mcp/manager.ts +17 -18
- package/src/mcp/tool-bridge.ts +4 -9
- package/src/mcp/tool-cache.ts +2 -3
- package/src/mcp/transports/http.ts +2 -4
- package/src/mcp/transports/stdio.ts +1 -2
- package/src/migrations.ts +63 -52
- package/src/modes/components/armin.ts +4 -5
- package/src/modes/components/assistant-message.ts +33 -5
- package/src/modes/components/bash-execution.ts +7 -8
- package/src/modes/components/bordered-loader.ts +3 -3
- package/src/modes/components/branch-summary-message.ts +3 -3
- package/src/modes/components/compaction-summary-message.ts +3 -3
- package/src/modes/components/countdown-timer.ts +0 -1
- package/src/modes/components/custom-message.ts +5 -5
- package/src/modes/components/diff.ts +1 -1
- package/src/modes/components/dynamic-border.ts +2 -2
- package/src/modes/components/extensions/extension-dashboard.ts +6 -7
- package/src/modes/components/extensions/extension-list.ts +2 -3
- package/src/modes/components/extensions/inspector-panel.ts +3 -4
- package/src/modes/components/extensions/state-manager.ts +25 -26
- package/src/modes/components/extensions/types.ts +1 -2
- package/src/modes/components/footer.ts +47 -43
- package/src/modes/components/history-search.ts +2 -2
- package/src/modes/components/hook-editor.ts +3 -4
- package/src/modes/components/hook-input.ts +2 -3
- package/src/modes/components/hook-message.ts +5 -5
- package/src/modes/components/hook-selector.ts +2 -3
- package/src/modes/components/keybinding-hints.ts +2 -3
- package/src/modes/components/login-dialog.ts +2 -2
- package/src/modes/components/model-selector.ts +12 -12
- package/src/modes/components/oauth-selector.ts +2 -2
- package/src/modes/components/plugin-settings.ts +20 -20
- package/src/modes/components/python-execution.ts +7 -8
- package/src/modes/components/queue-mode-selector.ts +3 -3
- package/src/modes/components/read-tool-group.ts +2 -2
- package/src/modes/components/session-selector.ts +4 -4
- package/src/modes/components/settings-defs.ts +77 -69
- package/src/modes/components/settings-selector.ts +16 -16
- package/src/modes/components/show-images-selector.ts +2 -2
- package/src/modes/components/status-line/segments.ts +4 -4
- package/src/modes/components/status-line/separators.ts +1 -1
- package/src/modes/components/status-line/types.ts +2 -2
- package/src/modes/components/status-line-segment-editor.ts +7 -8
- package/src/modes/components/status-line.ts +12 -12
- package/src/modes/components/theme-selector.ts +8 -7
- package/src/modes/components/thinking-selector.ts +4 -4
- package/src/modes/components/todo-display.ts +2 -2
- package/src/modes/components/todo-reminder.ts +4 -4
- package/src/modes/components/tool-execution.ts +16 -19
- package/src/modes/components/tree-selector.ts +12 -12
- package/src/modes/components/ttsr-notification.ts +5 -5
- package/src/modes/components/user-message-selector.ts +1 -1
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/components/visual-truncate.ts +0 -1
- package/src/modes/components/welcome.ts +4 -4
- package/src/modes/controllers/command-controller.ts +46 -47
- package/src/modes/controllers/event-controller.ts +16 -20
- package/src/modes/controllers/extension-ui-controller.ts +40 -46
- package/src/modes/controllers/input-controller.ts +17 -18
- package/src/modes/controllers/selector-controller.ts +103 -91
- package/src/modes/index.ts +3 -3
- package/src/modes/interactive-mode.ts +31 -31
- package/src/modes/print-mode.ts +12 -13
- package/src/modes/rpc/rpc-client.ts +7 -8
- package/src/modes/rpc/rpc-mode.ts +24 -28
- package/src/modes/rpc/rpc-types.ts +3 -4
- package/src/modes/theme/mermaid-cache.ts +89 -0
- package/src/modes/theme/theme.ts +130 -53
- package/src/modes/types.ts +10 -10
- package/src/modes/utils/ui-helpers.ts +17 -17
- package/src/patch/applicator.ts +18 -19
- package/src/patch/diff.ts +1 -2
- package/src/patch/fuzzy.ts +1 -2
- package/src/patch/index.ts +11 -18
- package/src/patch/normalize.ts +4 -4
- package/src/patch/normative.ts +1 -2
- package/src/patch/parser.ts +8 -9
- package/src/patch/shared.ts +43 -16
- package/src/prompts/tools/task.md +2 -0
- package/src/sdk.ts +100 -65
- package/src/session/agent-session.ts +84 -85
- package/src/session/agent-storage.ts +43 -39
- package/src/session/artifacts.ts +32 -10
- package/src/session/auth-storage.ts +50 -39
- package/src/session/compaction/branch-summarization.ts +7 -10
- package/src/session/compaction/compaction.ts +8 -19
- package/src/session/compaction/utils.ts +6 -9
- package/src/session/history-storage.ts +10 -10
- package/src/session/messages.ts +4 -5
- package/src/session/session-manager.ts +76 -65
- package/src/session/session-storage.ts +57 -69
- package/src/session/storage-migration.ts +14 -56
- package/src/session/streaming-output.ts +2 -2
- package/src/ssh/connection-manager.ts +43 -50
- package/src/ssh/ssh-executor.ts +2 -2
- package/src/ssh/sshfs-mount.ts +11 -18
- package/src/system-prompt.ts +28 -35
- package/src/task/agents.ts +45 -30
- package/src/task/commands.ts +6 -7
- package/src/task/discovery.ts +39 -76
- package/src/task/executor.ts +14 -15
- package/src/task/index.ts +40 -34
- package/src/task/output-manager.ts +93 -0
- package/src/task/parallel.ts +0 -1
- package/src/task/render.ts +24 -30
- package/src/task/subprocess-tool-registry.ts +1 -2
- package/src/task/worker-protocol.ts +3 -3
- package/src/task/worker.ts +33 -39
- package/src/task/worktree.ts +19 -19
- package/src/tools/ask.ts +41 -20
- package/src/tools/bash-interceptor.ts +1 -5
- package/src/tools/bash.ts +91 -97
- package/src/tools/calculator.ts +49 -47
- package/src/tools/complete.ts +4 -5
- package/src/tools/context.ts +2 -2
- package/src/tools/fetch.ts +84 -124
- package/src/tools/find.ts +94 -98
- package/src/tools/gemini-image.ts +14 -14
- package/src/tools/grep.ts +100 -116
- package/src/tools/index.ts +80 -55
- package/src/tools/list-limit.ts +1 -1
- package/src/tools/ls.ts +44 -70
- package/src/tools/notebook.ts +51 -67
- package/src/tools/output-meta.ts +3 -4
- package/src/tools/output-utils.ts +2 -2
- package/src/tools/path-utils.ts +5 -5
- package/src/tools/python.ts +104 -217
- package/src/tools/read.ts +92 -33
- package/src/tools/render-utils.ts +8 -23
- package/src/tools/renderers.ts +6 -7
- package/src/tools/review.ts +8 -11
- package/src/tools/ssh.ts +69 -49
- package/src/tools/todo-write.ts +37 -25
- package/src/tools/tool-errors.ts +3 -3
- package/src/tools/tool-result.ts +3 -8
- package/src/tools/write.ts +99 -75
- package/src/tui/code-cell.ts +109 -0
- package/src/tui/file-list.ts +47 -0
- package/src/tui/index.ts +11 -0
- package/src/tui/output-block.ts +72 -0
- package/src/tui/status-line.ts +39 -0
- package/src/tui/tree-list.ts +55 -0
- package/src/tui/types.ts +16 -0
- package/src/tui/utils.ts +48 -0
- package/src/utils/changelog.ts +9 -10
- package/src/utils/clipboard.ts +11 -11
- package/src/utils/file-mentions.ts +4 -10
- package/src/utils/frontmatter.ts +6 -3
- package/src/utils/fuzzy.ts +2 -2
- package/src/utils/image-convert.ts +1 -1
- package/src/utils/image-resize.ts +1 -1
- package/src/utils/mime.ts +2 -2
- package/src/utils/shell-snapshot.ts +11 -13
- package/src/utils/shell.ts +4 -5
- package/src/utils/title-generator.ts +8 -9
- package/src/utils/tools-manager.ts +23 -23
- package/src/vendor/photon/index.js +1099 -1059
- package/src/vendor/photon/photon_rs_bg.wasm +0 -0
- package/src/web/scrapers/artifacthub.ts +1 -1
- package/src/web/scrapers/arxiv.ts +2 -2
- package/src/web/scrapers/bluesky.ts +2 -2
- package/src/web/scrapers/cheatsh.ts +1 -1
- package/src/web/scrapers/chocolatey.ts +2 -2
- package/src/web/scrapers/choosealicense.ts +5 -5
- package/src/web/scrapers/cisa-kev.ts +1 -1
- package/src/web/scrapers/crossref.ts +2 -2
- package/src/web/scrapers/devto.ts +3 -3
- package/src/web/scrapers/discogs.ts +3 -4
- package/src/web/scrapers/discourse.ts +1 -1
- package/src/web/scrapers/dockerhub.ts +1 -1
- package/src/web/scrapers/fdroid.ts +2 -2
- package/src/web/scrapers/firefox-addons.ts +3 -3
- package/src/web/scrapers/flathub.ts +1 -1
- package/src/web/scrapers/github.ts +3 -3
- package/src/web/scrapers/gitlab.ts +4 -4
- package/src/web/scrapers/hackernews.ts +2 -2
- package/src/web/scrapers/huggingface.ts +1 -1
- package/src/web/scrapers/iacr.ts +2 -2
- package/src/web/scrapers/index.ts +0 -1
- package/src/web/scrapers/jetbrains-marketplace.ts +1 -1
- package/src/web/scrapers/lemmy.ts +2 -2
- package/src/web/scrapers/maven.ts +2 -2
- package/src/web/scrapers/mdn.ts +2 -4
- package/src/web/scrapers/metacpan.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/npm.ts +1 -1
- package/src/web/scrapers/nuget.ts +2 -2
- package/src/web/scrapers/nvd.ts +3 -3
- package/src/web/scrapers/ollama.ts +7 -9
- package/src/web/scrapers/opencorporates.ts +2 -2
- package/src/web/scrapers/openlibrary.ts +6 -6
- package/src/web/scrapers/orcid.ts +0 -1
- package/src/web/scrapers/osv.ts +2 -2
- package/src/web/scrapers/packagist.ts +1 -1
- package/src/web/scrapers/pubmed.ts +1 -2
- package/src/web/scrapers/rawg.ts +2 -2
- package/src/web/scrapers/readthedocs.ts +1 -2
- package/src/web/scrapers/repology.ts +2 -2
- package/src/web/scrapers/rfc.ts +1 -1
- package/src/web/scrapers/searchcode.ts +2 -2
- package/src/web/scrapers/semantic-scholar.ts +1 -1
- package/src/web/scrapers/snapcraft.ts +2 -2
- package/src/web/scrapers/sourcegraph.ts +1 -1
- package/src/web/scrapers/spdx.ts +3 -3
- package/src/web/scrapers/spotify.ts +0 -1
- package/src/web/scrapers/twitter.ts +1 -1
- package/src/web/scrapers/types.ts +1 -2
- package/src/web/scrapers/utils.ts +5 -5
- package/src/web/scrapers/wikidata.ts +3 -3
- package/src/web/scrapers/youtube.ts +9 -14
- package/src/web/search/auth.ts +5 -10
- package/src/web/search/index.ts +11 -21
- package/src/web/search/providers/anthropic.ts +3 -9
- package/src/web/search/providers/exa.ts +6 -10
- package/src/web/search/providers/perplexity.ts +5 -5
- package/src/web/search/render.ts +129 -175
- package/tsconfig.json +0 -42
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { logger } from "@oh-my-pi/pi-utils";
|
|
6
|
+
import type { FileDiff, FileHunks, NumstatEntry } from "../../commit/types";
|
|
7
|
+
import { parseDiffHunks, parseFileDiffs, parseFileHunks, parseNumstat } from "./diff";
|
|
8
|
+
import { GitError } from "./errors";
|
|
9
|
+
import { commit, push, resetStaging, runGitCommand, stageFiles } from "./operations";
|
|
10
|
+
|
|
11
|
+
export type HunkSelection = {
|
|
12
|
+
path: string;
|
|
13
|
+
hunks: { type: "all" } | { type: "indices"; indices: number[] } | { type: "lines"; start: number; end: number };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export class ControlledGit {
|
|
17
|
+
constructor(private readonly cwd: string) {}
|
|
18
|
+
|
|
19
|
+
async getDiff(staged: boolean): Promise<string> {
|
|
20
|
+
const args = staged ? ["diff", "--cached"] : ["diff"];
|
|
21
|
+
const result = await runGitCommand(this.cwd, args);
|
|
22
|
+
this.ensureSuccess(result, "git diff");
|
|
23
|
+
return result.stdout;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async getDiffForFiles(files: string[], staged = true): Promise<string> {
|
|
27
|
+
const args = staged ? ["diff", "--cached", "--", ...files] : ["diff", "--", ...files];
|
|
28
|
+
const result = await runGitCommand(this.cwd, args);
|
|
29
|
+
this.ensureSuccess(result, "git diff (files)");
|
|
30
|
+
return result.stdout;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getChangedFiles(staged: boolean): Promise<string[]> {
|
|
34
|
+
const args = staged ? ["diff", "--cached", "--name-only"] : ["diff", "--name-only"];
|
|
35
|
+
const result = await runGitCommand(this.cwd, args);
|
|
36
|
+
this.ensureSuccess(result, "git diff --name-only");
|
|
37
|
+
return result.stdout
|
|
38
|
+
.split("\n")
|
|
39
|
+
.map(line => line.trim())
|
|
40
|
+
.filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async getStat(staged: boolean): Promise<string> {
|
|
44
|
+
const args = staged ? ["diff", "--cached", "--stat"] : ["diff", "--stat"];
|
|
45
|
+
const result = await runGitCommand(this.cwd, args);
|
|
46
|
+
this.ensureSuccess(result, "git diff --stat");
|
|
47
|
+
return result.stdout;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getStatForFiles(files: string[], staged = true): Promise<string> {
|
|
51
|
+
const args = staged ? ["diff", "--cached", "--stat", "--", ...files] : ["diff", "--stat", "--", ...files];
|
|
52
|
+
const result = await runGitCommand(this.cwd, args);
|
|
53
|
+
this.ensureSuccess(result, "git diff --stat (files)");
|
|
54
|
+
return result.stdout;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getNumstat(staged: boolean): Promise<NumstatEntry[]> {
|
|
58
|
+
const args = staged ? ["diff", "--cached", "--numstat"] : ["diff", "--numstat"];
|
|
59
|
+
const result = await runGitCommand(this.cwd, args);
|
|
60
|
+
this.ensureSuccess(result, "git diff --numstat");
|
|
61
|
+
return parseNumstat(result.stdout);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async getRecentCommits(count: number): Promise<string[]> {
|
|
65
|
+
const result = await runGitCommand(this.cwd, ["log", `-n${count}`, "--pretty=format:%s"]);
|
|
66
|
+
this.ensureSuccess(result, "git log");
|
|
67
|
+
return result.stdout
|
|
68
|
+
.split("\n")
|
|
69
|
+
.map(line => line.trim())
|
|
70
|
+
.filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async getStagedFiles(): Promise<string[]> {
|
|
74
|
+
const result = await runGitCommand(this.cwd, ["diff", "--cached", "--name-only"]);
|
|
75
|
+
this.ensureSuccess(result, "git diff --cached --name-only");
|
|
76
|
+
return result.stdout
|
|
77
|
+
.split("\n")
|
|
78
|
+
.map(line => line.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async getUntrackedFiles(): Promise<string[]> {
|
|
83
|
+
const result = await runGitCommand(this.cwd, ["ls-files", "--others", "--exclude-standard"]);
|
|
84
|
+
this.ensureSuccess(result, "git ls-files --others --exclude-standard");
|
|
85
|
+
return result.stdout
|
|
86
|
+
.split("\n")
|
|
87
|
+
.map(line => line.trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async stageAll(): Promise<void> {
|
|
92
|
+
const result = await stageFiles(this.cwd, []);
|
|
93
|
+
this.ensureSuccess(result, "git add -A");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async stageFiles(files: string[]): Promise<void> {
|
|
97
|
+
const result = await stageFiles(this.cwd, files);
|
|
98
|
+
this.ensureSuccess(result, "git add");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async stageHunks(selections: HunkSelection[]): Promise<void> {
|
|
102
|
+
if (selections.length === 0) return;
|
|
103
|
+
const diff = await this.getDiff(false);
|
|
104
|
+
const fileDiffs = parseFileDiffs(diff);
|
|
105
|
+
const fileDiffMap = new Map(fileDiffs.map(entry => [entry.filename, entry]));
|
|
106
|
+
const patchParts: string[] = [];
|
|
107
|
+
for (const selection of selections) {
|
|
108
|
+
const fileDiff = fileDiffMap.get(selection.path);
|
|
109
|
+
if (!fileDiff) {
|
|
110
|
+
throw new GitError("git apply --cached", `No diff found for ${selection.path}`);
|
|
111
|
+
}
|
|
112
|
+
if (fileDiff.isBinary) {
|
|
113
|
+
if (selection.hunks.type !== "all") {
|
|
114
|
+
throw new GitError("git apply --cached", `Cannot select hunks for binary file ${selection.path}`);
|
|
115
|
+
}
|
|
116
|
+
patchParts.push(fileDiff.content);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (selection.hunks.type === "all") {
|
|
121
|
+
patchParts.push(fileDiff.content);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const fileHunks = parseFileHunks(fileDiff);
|
|
126
|
+
const selectedHunks = selectHunks(fileHunks, selection.hunks);
|
|
127
|
+
if (selectedHunks.length === 0) {
|
|
128
|
+
throw new GitError("git apply --cached", `No hunks selected for ${selection.path}`);
|
|
129
|
+
}
|
|
130
|
+
const header = extractFileHeader(fileDiff.content);
|
|
131
|
+
const filePatch = [header, ...selectedHunks.map(hunk => hunk.content)].join("\n");
|
|
132
|
+
patchParts.push(filePatch);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const patch = joinPatch(patchParts);
|
|
136
|
+
if (!patch.trim()) return;
|
|
137
|
+
const tempPath = path.join(os.tmpdir(), `omp-hunks-${randomUUID()}.patch`);
|
|
138
|
+
try {
|
|
139
|
+
await Bun.write(tempPath, patch);
|
|
140
|
+
const result = await runGitCommand(this.cwd, ["apply", "--cached", "--binary", tempPath]);
|
|
141
|
+
this.ensureSuccess(result, "git apply --cached");
|
|
142
|
+
} finally {
|
|
143
|
+
await fs.rm(tempPath, { force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async resetStaging(files: string[] = []): Promise<void> {
|
|
148
|
+
const result = await resetStaging(this.cwd, files);
|
|
149
|
+
this.ensureSuccess(result, "git reset");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async commit(message: string): Promise<void> {
|
|
153
|
+
const result = await commit(this.cwd, message);
|
|
154
|
+
this.ensureSuccess(result, "git commit");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async push(): Promise<void> {
|
|
158
|
+
const result = await push(this.cwd);
|
|
159
|
+
this.ensureSuccess(result, "git push");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
parseDiffFiles(diff: string): FileDiff[] {
|
|
163
|
+
return parseFileDiffs(diff);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
parseDiffHunks(diff: string): FileHunks[] {
|
|
167
|
+
return parseDiffHunks(diff);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async getHunks(files: string[], staged = true): Promise<FileHunks[]> {
|
|
171
|
+
const diff = await this.getDiffForFiles(files, staged);
|
|
172
|
+
return this.parseDiffHunks(diff);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private ensureSuccess(result: { exitCode: number; stderr: string }, label: string): void {
|
|
176
|
+
if (result.exitCode !== 0) {
|
|
177
|
+
logger.error("commit git command failed", { label, stderr: result.stderr });
|
|
178
|
+
throw new GitError(label, result.stderr);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function extractFileHeader(diff: string): string {
|
|
184
|
+
const lines = diff.split("\n");
|
|
185
|
+
const headerLines: string[] = [];
|
|
186
|
+
for (const line of lines) {
|
|
187
|
+
if (line.startsWith("@@")) break;
|
|
188
|
+
headerLines.push(line);
|
|
189
|
+
}
|
|
190
|
+
return headerLines.join("\n");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function joinPatch(parts: string[]): string {
|
|
194
|
+
return parts
|
|
195
|
+
.map(part => (part.endsWith("\n") ? part : `${part}\n`))
|
|
196
|
+
.join("\n")
|
|
197
|
+
.trimEnd()
|
|
198
|
+
.concat("\n");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function selectHunks(file: FileHunks, selector: HunkSelection["hunks"]): FileHunks["hunks"] {
|
|
202
|
+
if (selector.type === "indices") {
|
|
203
|
+
const wanted = new Set(selector.indices.map(value => Math.max(1, Math.floor(value))));
|
|
204
|
+
return file.hunks.filter(hunk => wanted.has(hunk.index + 1));
|
|
205
|
+
}
|
|
206
|
+
if (selector.type === "lines") {
|
|
207
|
+
const start = Math.floor(selector.start);
|
|
208
|
+
const end = Math.floor(selector.end);
|
|
209
|
+
return file.hunks.filter(hunk => hunk.newStart <= end && hunk.newStart + hunk.newLines - 1 >= start);
|
|
210
|
+
}
|
|
211
|
+
return file.hunks;
|
|
212
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
|
|
3
|
+
interface GitResult {
|
|
4
|
+
exitCode: number;
|
|
5
|
+
stdout: string;
|
|
6
|
+
stderr: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function runGitCommand(cwd: string, args: string[]): Promise<GitResult> {
|
|
10
|
+
const result = await $`git ${args}`.cwd(cwd).quiet().nothrow();
|
|
11
|
+
const stdout = result.text();
|
|
12
|
+
const stderr = result.stderr?.toString() ?? "";
|
|
13
|
+
return {
|
|
14
|
+
exitCode: result.exitCode ?? 0,
|
|
15
|
+
stdout,
|
|
16
|
+
stderr,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function stageFiles(cwd: string, files: string[]): Promise<GitResult> {
|
|
21
|
+
const args = files.length === 0 ? ["add", "-A"] : ["add", "--", ...files];
|
|
22
|
+
return runGitCommand(cwd, args);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function resetStaging(cwd: string, files: string[]): Promise<GitResult> {
|
|
26
|
+
const args = files.length === 0 ? ["reset"] : ["reset", "--", ...files];
|
|
27
|
+
return runGitCommand(cwd, args);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function push(cwd: string): Promise<GitResult> {
|
|
31
|
+
return runGitCommand(cwd, ["push"]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function commit(cwd: string, message: string): Promise<GitResult> {
|
|
35
|
+
const child = Bun.spawn(["git", "commit", "-F", "-"], {
|
|
36
|
+
cwd,
|
|
37
|
+
stdin: Buffer.from(message),
|
|
38
|
+
stdout: "pipe",
|
|
39
|
+
stderr: "pipe",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
43
|
+
new Response(child.stdout).text(),
|
|
44
|
+
new Response(child.stderr).text(),
|
|
45
|
+
child.exited,
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
exitCode: exitCode ?? 0,
|
|
50
|
+
stdout: stdout.trim(),
|
|
51
|
+
stderr: stderr.trim(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Api, Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { parseFileDiffs } from "../../commit/git/diff";
|
|
3
|
+
import type { ConventionalAnalysis } from "../../commit/types";
|
|
4
|
+
import { isExcludedFile } from "../../commit/utils/exclusions";
|
|
5
|
+
import { runMapPhase } from "./map-phase";
|
|
6
|
+
import { runReducePhase } from "./reduce-phase";
|
|
7
|
+
import { estimateTokens } from "./utils";
|
|
8
|
+
|
|
9
|
+
const MIN_FILES_FOR_MAP_REDUCE = 4;
|
|
10
|
+
const MAX_FILE_TOKENS = 50_000;
|
|
11
|
+
|
|
12
|
+
export interface MapReduceSettings {
|
|
13
|
+
enabled?: boolean;
|
|
14
|
+
minFiles?: number;
|
|
15
|
+
maxFileTokens?: number;
|
|
16
|
+
maxConcurrency?: number;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface MapReduceInput {
|
|
21
|
+
model: Model<Api>;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
smolModel: Model<Api>;
|
|
24
|
+
smolApiKey: string;
|
|
25
|
+
diff: string;
|
|
26
|
+
stat: string;
|
|
27
|
+
scopeCandidates: string;
|
|
28
|
+
typesDescription?: string;
|
|
29
|
+
settings?: MapReduceSettings;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function shouldUseMapReduce(diff: string, settings?: MapReduceSettings): boolean {
|
|
33
|
+
if (process.env.OMP_COMMIT_MAP_REDUCE?.toLowerCase() === "false") return false;
|
|
34
|
+
if (settings?.enabled === false) return false;
|
|
35
|
+
const minFiles = settings?.minFiles ?? MIN_FILES_FOR_MAP_REDUCE;
|
|
36
|
+
const maxFileTokens = settings?.maxFileTokens ?? MAX_FILE_TOKENS;
|
|
37
|
+
const files = parseFileDiffs(diff).filter(file => !isExcludedFile(file.filename));
|
|
38
|
+
const fileCount = files.length;
|
|
39
|
+
if (fileCount >= minFiles) return true;
|
|
40
|
+
return files.some(file => estimateTokens(file.content) > maxFileTokens);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Run map-reduce analysis for large diffs using smol + primary models.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
export async function runMapReduceAnalysis(input: MapReduceInput): Promise<ConventionalAnalysis> {
|
|
48
|
+
const fileDiffs = parseFileDiffs(input.diff).filter(file => !isExcludedFile(file.filename));
|
|
49
|
+
const observations = await runMapPhase({
|
|
50
|
+
model: input.smolModel,
|
|
51
|
+
apiKey: input.smolApiKey,
|
|
52
|
+
files: fileDiffs,
|
|
53
|
+
config: input.settings,
|
|
54
|
+
});
|
|
55
|
+
return runReducePhase({
|
|
56
|
+
model: input.model,
|
|
57
|
+
apiKey: input.apiKey,
|
|
58
|
+
observations,
|
|
59
|
+
stat: input.stat,
|
|
60
|
+
scopeCandidates: input.scopeCandidates,
|
|
61
|
+
typesDescription: input.typesDescription,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import type { Api, AssistantMessage, Message, Model } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import fileObserverSystemPrompt from "../../commit/prompts/file-observer-system.md" with { type: "text" };
|
|
4
|
+
import fileObserverUserPrompt from "../../commit/prompts/file-observer-user.md" with { type: "text" };
|
|
5
|
+
import type { FileDiff, FileObservation } from "../../commit/types";
|
|
6
|
+
import { isExcludedFile } from "../../commit/utils/exclusions";
|
|
7
|
+
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
8
|
+
import { truncateToTokenLimit } from "./utils";
|
|
9
|
+
|
|
10
|
+
const MAX_FILE_TOKENS = 50_000;
|
|
11
|
+
const MAX_CONTEXT_FILES = 20;
|
|
12
|
+
const MAX_CONCURRENCY = 5;
|
|
13
|
+
const MAP_PHASE_TIMEOUT_MS = 120_000;
|
|
14
|
+
const MAX_RETRIES = 3;
|
|
15
|
+
const RETRY_BACKOFF_MS = 1000;
|
|
16
|
+
|
|
17
|
+
export interface MapPhaseInput {
|
|
18
|
+
model: Model<Api>;
|
|
19
|
+
apiKey: string;
|
|
20
|
+
files: FileDiff[];
|
|
21
|
+
config?: {
|
|
22
|
+
maxFileTokens?: number;
|
|
23
|
+
maxConcurrency?: number;
|
|
24
|
+
timeoutMs?: number;
|
|
25
|
+
maxRetries?: number;
|
|
26
|
+
retryBackoffMs?: number;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function runMapPhase({ model, apiKey, files, config }: MapPhaseInput): Promise<FileObservation[]> {
|
|
31
|
+
const filtered = files.filter(file => !isExcludedFile(file.filename));
|
|
32
|
+
const systemPrompt = renderPromptTemplate(fileObserverSystemPrompt);
|
|
33
|
+
const maxFileTokens = config?.maxFileTokens ?? MAX_FILE_TOKENS;
|
|
34
|
+
const maxConcurrency = config?.maxConcurrency ?? MAX_CONCURRENCY;
|
|
35
|
+
const timeoutMs = config?.timeoutMs ?? MAP_PHASE_TIMEOUT_MS;
|
|
36
|
+
const maxRetries = config?.maxRetries ?? MAX_RETRIES;
|
|
37
|
+
const retryBackoffMs = config?.retryBackoffMs ?? RETRY_BACKOFF_MS;
|
|
38
|
+
return runWithConcurrency(filtered, maxConcurrency, async file => {
|
|
39
|
+
if (file.isBinary) {
|
|
40
|
+
return {
|
|
41
|
+
file: file.filename,
|
|
42
|
+
observations: ["Binary file changed."],
|
|
43
|
+
additions: file.additions,
|
|
44
|
+
deletions: file.deletions,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const contextHeader = generateContextHeader(filtered, file.filename);
|
|
49
|
+
const truncated = truncateToTokenLimit(file.content, maxFileTokens);
|
|
50
|
+
const prompt = renderPromptTemplate(fileObserverUserPrompt, {
|
|
51
|
+
filename: file.filename,
|
|
52
|
+
diff: truncated,
|
|
53
|
+
context_header: contextHeader,
|
|
54
|
+
});
|
|
55
|
+
const request = {
|
|
56
|
+
systemPrompt,
|
|
57
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }] as Message[],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const response = await withRetry(
|
|
61
|
+
() => completeSimple(model, request, { apiKey, maxTokens: 400, signal: AbortSignal.timeout(timeoutMs) }),
|
|
62
|
+
maxRetries,
|
|
63
|
+
retryBackoffMs,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const observations = parseObservations(response);
|
|
67
|
+
return {
|
|
68
|
+
file: file.filename,
|
|
69
|
+
observations,
|
|
70
|
+
additions: file.additions,
|
|
71
|
+
deletions: file.deletions,
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function parseObservations(message: AssistantMessage): string[] {
|
|
77
|
+
const text = message.content
|
|
78
|
+
.filter(content => content.type === "text")
|
|
79
|
+
.map(content => content.text)
|
|
80
|
+
.join("")
|
|
81
|
+
.trim();
|
|
82
|
+
|
|
83
|
+
if (!text) return [];
|
|
84
|
+
|
|
85
|
+
const lines = text
|
|
86
|
+
.split("\n")
|
|
87
|
+
.map(line => line.trim())
|
|
88
|
+
.filter(Boolean)
|
|
89
|
+
.map(line => line.replace(/^[-*]\s+/, ""))
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
|
|
92
|
+
return lines.slice(0, 5);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function generateContextHeader(files: FileDiff[], currentFile: string): string {
|
|
96
|
+
if (files.length > 100) {
|
|
97
|
+
return `(Large commit with ${files.length} total files)`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const otherFiles = files.filter(file => file.filename !== currentFile);
|
|
101
|
+
if (otherFiles.length === 0) return "";
|
|
102
|
+
|
|
103
|
+
const sorted = [...otherFiles].sort((a, b) => b.additions + b.deletions - (a.additions + a.deletions));
|
|
104
|
+
const toShow = sorted.length > MAX_CONTEXT_FILES ? sorted.slice(0, MAX_CONTEXT_FILES) : sorted;
|
|
105
|
+
|
|
106
|
+
const lines = ["OTHER FILES IN THIS CHANGE:"];
|
|
107
|
+
for (const file of toShow) {
|
|
108
|
+
const lineCount = file.additions + file.deletions;
|
|
109
|
+
const description = inferFileDescription(file);
|
|
110
|
+
lines.push(`- ${file.filename} (${lineCount} lines): ${description}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (toShow.length < sorted.length) {
|
|
114
|
+
lines.push(`... and ${sorted.length - toShow.length} more files`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines.join("\n");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function inferFileDescription(file: FileDiff): string {
|
|
121
|
+
const filenameLower = file.filename.toLowerCase();
|
|
122
|
+
if (filenameLower.includes("test")) return "test file";
|
|
123
|
+
if (filenameLower.endsWith(".md")) return "documentation";
|
|
124
|
+
if (
|
|
125
|
+
filenameLower.includes("config") ||
|
|
126
|
+
filenameLower.endsWith(".toml") ||
|
|
127
|
+
filenameLower.endsWith(".yaml") ||
|
|
128
|
+
filenameLower.endsWith(".yml")
|
|
129
|
+
) {
|
|
130
|
+
return "configuration";
|
|
131
|
+
}
|
|
132
|
+
if (filenameLower.includes("error")) return "error definitions";
|
|
133
|
+
if (filenameLower.includes("type")) return "type definitions";
|
|
134
|
+
if (filenameLower.endsWith("mod.rs") || filenameLower.endsWith("lib.rs")) return "module exports";
|
|
135
|
+
if (filenameLower.endsWith("main.rs") || filenameLower.endsWith("main.go") || filenameLower.endsWith("main.py")) {
|
|
136
|
+
return "entry point";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const content = file.content;
|
|
140
|
+
if (content.includes("interface ") || content.includes("type ")) return "type definitions";
|
|
141
|
+
if (content.includes("class ") || content.includes("function ") || content.includes("fn ")) return "implementation";
|
|
142
|
+
if (content.includes("async ") || content.includes("await")) return "async code";
|
|
143
|
+
return "source code";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runWithConcurrency<T, R>(
|
|
147
|
+
items: T[],
|
|
148
|
+
limit: number,
|
|
149
|
+
worker: (item: T, index: number) => Promise<R>,
|
|
150
|
+
): Promise<R[]> {
|
|
151
|
+
const results = new Array<R>(items.length);
|
|
152
|
+
let nextIndex = 0;
|
|
153
|
+
const runners = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
154
|
+
while (true) {
|
|
155
|
+
const current = nextIndex;
|
|
156
|
+
nextIndex += 1;
|
|
157
|
+
if (current >= items.length) return;
|
|
158
|
+
results[current] = await worker(items[current] as T, current);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
await Promise.all(runners);
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function withRetry<T>(fn: () => Promise<T>, attempts: number, backoffMs: number): Promise<T> {
|
|
166
|
+
let lastError: unknown;
|
|
167
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
168
|
+
try {
|
|
169
|
+
return await fn();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
lastError = error;
|
|
172
|
+
if (attempt < attempts - 1) {
|
|
173
|
+
await Bun.sleep(backoffMs * (attempt + 1));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw lastError;
|
|
178
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Api, AssistantMessage, Model, ToolCall } from "@oh-my-pi/pi-ai";
|
|
2
|
+
import { completeSimple, validateToolCall } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import reduceSystemPrompt from "../../commit/prompts/reduce-system.md" with { type: "text" };
|
|
5
|
+
import reduceUserPrompt from "../../commit/prompts/reduce-user.md" with { type: "text" };
|
|
6
|
+
import type { ChangelogCategory, ConventionalAnalysis, FileObservation } from "../../commit/types";
|
|
7
|
+
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
8
|
+
|
|
9
|
+
const ReduceTool = {
|
|
10
|
+
name: "create_conventional_analysis",
|
|
11
|
+
description: "Synthesize file observations into a conventional commit analysis.",
|
|
12
|
+
parameters: Type.Object({
|
|
13
|
+
type: Type.Union([
|
|
14
|
+
Type.Literal("feat"),
|
|
15
|
+
Type.Literal("fix"),
|
|
16
|
+
Type.Literal("refactor"),
|
|
17
|
+
Type.Literal("docs"),
|
|
18
|
+
Type.Literal("test"),
|
|
19
|
+
Type.Literal("chore"),
|
|
20
|
+
Type.Literal("style"),
|
|
21
|
+
Type.Literal("perf"),
|
|
22
|
+
Type.Literal("build"),
|
|
23
|
+
Type.Literal("ci"),
|
|
24
|
+
Type.Literal("revert"),
|
|
25
|
+
]),
|
|
26
|
+
scope: Type.Union([Type.String(), Type.Null()]),
|
|
27
|
+
details: Type.Array(
|
|
28
|
+
Type.Object({
|
|
29
|
+
text: Type.String(),
|
|
30
|
+
changelog_category: Type.Optional(
|
|
31
|
+
Type.Union([
|
|
32
|
+
Type.Literal("Added"),
|
|
33
|
+
Type.Literal("Changed"),
|
|
34
|
+
Type.Literal("Fixed"),
|
|
35
|
+
Type.Literal("Deprecated"),
|
|
36
|
+
Type.Literal("Removed"),
|
|
37
|
+
Type.Literal("Security"),
|
|
38
|
+
Type.Literal("Breaking Changes"),
|
|
39
|
+
]),
|
|
40
|
+
),
|
|
41
|
+
user_visible: Type.Optional(Type.Boolean()),
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
44
|
+
issue_refs: Type.Array(Type.String()),
|
|
45
|
+
}),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export interface ReducePhaseInput {
|
|
49
|
+
model: Model<Api>;
|
|
50
|
+
apiKey: string;
|
|
51
|
+
observations: FileObservation[];
|
|
52
|
+
stat: string;
|
|
53
|
+
scopeCandidates: string;
|
|
54
|
+
typesDescription?: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function runReducePhase({
|
|
58
|
+
model,
|
|
59
|
+
apiKey,
|
|
60
|
+
observations,
|
|
61
|
+
stat,
|
|
62
|
+
scopeCandidates,
|
|
63
|
+
typesDescription,
|
|
64
|
+
}: ReducePhaseInput): Promise<ConventionalAnalysis> {
|
|
65
|
+
const prompt = renderPromptTemplate(reduceUserPrompt, {
|
|
66
|
+
types_description: typesDescription,
|
|
67
|
+
observations: observations.flatMap(obs => obs.observations.map(line => `- ${obs.file}: ${line}`)).join("\n"),
|
|
68
|
+
stat,
|
|
69
|
+
scope_candidates: scopeCandidates,
|
|
70
|
+
});
|
|
71
|
+
const response = await completeSimple(
|
|
72
|
+
model,
|
|
73
|
+
{
|
|
74
|
+
systemPrompt: renderPromptTemplate(reduceSystemPrompt),
|
|
75
|
+
messages: [{ role: "user", content: prompt, timestamp: Date.now() }],
|
|
76
|
+
tools: [ReduceTool],
|
|
77
|
+
},
|
|
78
|
+
{ apiKey, maxTokens: 2400 },
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return parseAnalysisResponse(response);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function parseAnalysisResponse(message: AssistantMessage): ConventionalAnalysis {
|
|
85
|
+
const toolCall = extractToolCall(message, "create_conventional_analysis");
|
|
86
|
+
if (toolCall) {
|
|
87
|
+
const parsed = validateToolCall([ReduceTool], toolCall) as {
|
|
88
|
+
type: ConventionalAnalysis["type"];
|
|
89
|
+
scope: string | null;
|
|
90
|
+
details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
|
|
91
|
+
issue_refs: string[];
|
|
92
|
+
};
|
|
93
|
+
return normalizeAnalysis(parsed);
|
|
94
|
+
}
|
|
95
|
+
const text = extractTextContent(message);
|
|
96
|
+
const parsed = parseJsonPayload(text) as {
|
|
97
|
+
type: ConventionalAnalysis["type"];
|
|
98
|
+
scope: string | null;
|
|
99
|
+
details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
|
|
100
|
+
issue_refs: string[];
|
|
101
|
+
};
|
|
102
|
+
return normalizeAnalysis(parsed);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseJsonPayload(text: string): unknown {
|
|
106
|
+
const trimmed = text.trim();
|
|
107
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
108
|
+
return JSON.parse(trimmed) as unknown;
|
|
109
|
+
}
|
|
110
|
+
const match = trimmed.match(/\{[\s\S]*\}/);
|
|
111
|
+
if (!match) {
|
|
112
|
+
throw new Error("No JSON payload found in reduce response");
|
|
113
|
+
}
|
|
114
|
+
return JSON.parse(match[0]) as unknown;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeAnalysis(parsed: {
|
|
118
|
+
type: ConventionalAnalysis["type"];
|
|
119
|
+
scope: string | null;
|
|
120
|
+
details: Array<{ text: string; changelog_category?: ChangelogCategory; user_visible?: boolean }>;
|
|
121
|
+
issue_refs: string[];
|
|
122
|
+
}): ConventionalAnalysis {
|
|
123
|
+
return {
|
|
124
|
+
type: parsed.type,
|
|
125
|
+
scope: parsed.scope?.trim() || null,
|
|
126
|
+
details: parsed.details.map(detail => ({
|
|
127
|
+
text: detail.text.trim(),
|
|
128
|
+
changelogCategory: detail.user_visible ? detail.changelog_category : undefined,
|
|
129
|
+
userVisible: detail.user_visible ?? false,
|
|
130
|
+
})),
|
|
131
|
+
issueRefs: parsed.issue_refs ?? [],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function extractToolCall(message: AssistantMessage, name: string): ToolCall | undefined {
|
|
136
|
+
return message.content.find(content => content.type === "toolCall" && content.name === name) as ToolCall | undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function extractTextContent(message: AssistantMessage): string {
|
|
140
|
+
return message.content
|
|
141
|
+
.filter(content => content.type === "text")
|
|
142
|
+
.map(content => content.text)
|
|
143
|
+
.join("")
|
|
144
|
+
.trim();
|
|
145
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function estimateTokens(text: string): number {
|
|
2
|
+
return Math.ceil(text.length / 4);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function truncateToTokenLimit(text: string, maxTokens: number): string {
|
|
6
|
+
const maxChars = maxTokens * 4;
|
|
7
|
+
if (text.length <= maxChars) return text;
|
|
8
|
+
return `${text.slice(0, maxChars)}\n... (truncated)`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConventionalAnalysis } from "./types";
|
|
2
|
+
|
|
3
|
+
export function formatCommitMessage(analysis: ConventionalAnalysis, summary: string): string {
|
|
4
|
+
const scopePart = analysis.scope ? `(${analysis.scope})` : "";
|
|
5
|
+
const header = `${analysis.type}${scopePart}: ${summary}`;
|
|
6
|
+
const bodyLines = analysis.details.map(detail => `- ${detail.text.trim()}`);
|
|
7
|
+
if (bodyLines.length === 0) {
|
|
8
|
+
return header;
|
|
9
|
+
}
|
|
10
|
+
return `${header}\n\n${bodyLines.join("\n")}`;
|
|
11
|
+
}
|