@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
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as path from "node:path";
|
|
2
2
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
3
3
|
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
4
|
-
import {
|
|
5
|
-
import { resizeImage } from "@oh-my-pi/pi-coding-agent/utils/image-resize";
|
|
6
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
7
5
|
import { nanoid } from "nanoid";
|
|
6
|
+
import { getAgentDir as getDefaultAgentDir } from "../config";
|
|
7
|
+
import { resizeImage } from "../utils/image-resize";
|
|
8
8
|
import {
|
|
9
9
|
type BashExecutionMessage,
|
|
10
10
|
type CustomMessage,
|
|
@@ -252,7 +252,7 @@ function migrateV2ToV3(entries: FileEntry[]): void {
|
|
|
252
252
|
* Mutates entries in place. Returns true if any migration was applied.
|
|
253
253
|
*/
|
|
254
254
|
function migrateToCurrentVersion(entries: FileEntry[]): boolean {
|
|
255
|
-
const header = entries.find(
|
|
255
|
+
const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
256
256
|
const version = header?.version ?? 1;
|
|
257
257
|
|
|
258
258
|
if (version >= CURRENT_SESSION_VERSION) return false;
|
|
@@ -397,7 +397,7 @@ export function buildSessionContext(
|
|
|
397
397
|
messages.push(createCompactionSummaryMessage(compaction.summary, compaction.tokensBefore, compaction.timestamp));
|
|
398
398
|
|
|
399
399
|
// Find compaction index in path
|
|
400
|
-
const compactionIdx = path.findIndex(
|
|
400
|
+
const compactionIdx = path.findIndex(e => e.type === "compaction" && e.id === compaction.id);
|
|
401
401
|
|
|
402
402
|
// Emit kept messages (before compaction, starting from firstKeptEntryId)
|
|
403
403
|
let foundFirstKept = false;
|
|
@@ -432,16 +432,24 @@ export function buildSessionContext(
|
|
|
432
432
|
*/
|
|
433
433
|
function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
|
|
434
434
|
const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
435
|
-
const sessionDir = join(getDefaultAgentDir(), "sessions", safePath);
|
|
435
|
+
const sessionDir = path.join(getDefaultAgentDir(), "sessions", safePath);
|
|
436
436
|
storage.ensureDirSync(sessionDir);
|
|
437
437
|
return sessionDir;
|
|
438
438
|
}
|
|
439
439
|
|
|
440
440
|
/** Exported for testing */
|
|
441
|
-
export function loadEntriesFromFile(
|
|
442
|
-
|
|
441
|
+
export async function loadEntriesFromFile(
|
|
442
|
+
filePath: string,
|
|
443
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
444
|
+
): Promise<FileEntry[]> {
|
|
445
|
+
let content: string;
|
|
446
|
+
try {
|
|
447
|
+
content = await storage.readText(filePath);
|
|
448
|
+
} catch (err) {
|
|
449
|
+
if (isEnoent(err)) return [];
|
|
450
|
+
throw err;
|
|
451
|
+
}
|
|
443
452
|
|
|
444
|
-
const content = storage.readTextSync(filePath);
|
|
445
453
|
const entries: FileEntry[] = [];
|
|
446
454
|
const lines = content.trim().split("\n");
|
|
447
455
|
|
|
@@ -555,15 +563,13 @@ function extractFirstUserPrompt(lines: string[]): string | undefined {
|
|
|
555
563
|
* Uses low-level file I/O to efficiently read only the first 4KB of each file
|
|
556
564
|
* to extract the JSON header and first user message without loading entire session logs into memory.
|
|
557
565
|
*/
|
|
558
|
-
function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentSessionInfo[] {
|
|
566
|
+
async function getSortedSessions(sessionDir: string, storage: SessionStorage): Promise<RecentSessionInfo[]> {
|
|
559
567
|
try {
|
|
560
|
-
const buf = Buffer.alloc(4096);
|
|
561
568
|
const files: string[] = storage.listFilesSync(sessionDir, "*.jsonl");
|
|
562
|
-
|
|
563
|
-
.map((path: string) => {
|
|
569
|
+
const results = await Promise.all(
|
|
570
|
+
files.map(async (path: string) => {
|
|
564
571
|
try {
|
|
565
|
-
const
|
|
566
|
-
const content = buf.toString("utf-8", 0, length);
|
|
572
|
+
const content = await storage.readTextPrefix(path, 4096);
|
|
567
573
|
const lines = content.split("\n");
|
|
568
574
|
const firstLine = lines[0];
|
|
569
575
|
if (!firstLine || !firstLine.trim()) return null;
|
|
@@ -575,20 +581,20 @@ function getSortedSessions(sessionDir: string, storage: SessionStorage): RecentS
|
|
|
575
581
|
} catch {
|
|
576
582
|
return null;
|
|
577
583
|
}
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
|
|
584
|
+
}),
|
|
585
|
+
);
|
|
586
|
+
return results.filter((item): item is RecentSessionInfo => item !== null).sort((a, b) => b.mtime - a.mtime);
|
|
581
587
|
} catch {
|
|
582
588
|
return [];
|
|
583
589
|
}
|
|
584
590
|
}
|
|
585
591
|
|
|
586
592
|
/** Exported for testing */
|
|
587
|
-
export function findMostRecentSession(
|
|
593
|
+
export async function findMostRecentSession(
|
|
588
594
|
sessionDir: string,
|
|
589
595
|
storage: SessionStorage = new FileSessionStorage(),
|
|
590
|
-
): string | null {
|
|
591
|
-
const sessions = getSortedSessions(sessionDir, storage);
|
|
596
|
+
): Promise<string | null> {
|
|
597
|
+
const sessions = await getSortedSessions(sessionDir, storage);
|
|
592
598
|
return sessions[0]?.path || null;
|
|
593
599
|
}
|
|
594
600
|
|
|
@@ -676,7 +682,7 @@ async function truncateForPersistence<T>(obj: T, key?: string): Promise<T> {
|
|
|
676
682
|
if (Array.isArray(obj)) {
|
|
677
683
|
let changed = false;
|
|
678
684
|
const result = await Promise.all(
|
|
679
|
-
obj.map(async
|
|
685
|
+
obj.map(async item => {
|
|
680
686
|
// Special handling: compress oversized images while preserving shape
|
|
681
687
|
if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
|
|
682
688
|
if (item.data.length > MAX_PERSIST_CHARS) {
|
|
@@ -843,12 +849,13 @@ class NdjsonFileWriter {
|
|
|
843
849
|
}
|
|
844
850
|
|
|
845
851
|
/** Get recent sessions for display in welcome screen */
|
|
846
|
-
export function getRecentSessions(
|
|
852
|
+
export async function getRecentSessions(
|
|
847
853
|
sessionDir: string,
|
|
848
854
|
limit = 3,
|
|
849
855
|
storage: SessionStorage = new FileSessionStorage(),
|
|
850
|
-
): RecentSessionInfo[] {
|
|
851
|
-
|
|
856
|
+
): Promise<RecentSessionInfo[]> {
|
|
857
|
+
const sessions = await getSortedSessions(sessionDir, storage);
|
|
858
|
+
return sessions.slice(0, limit);
|
|
852
859
|
}
|
|
853
860
|
|
|
854
861
|
/**
|
|
@@ -882,16 +889,16 @@ function extractTextFromContent(content: Message["content"]): string {
|
|
|
882
889
|
if (typeof content === "string") return content;
|
|
883
890
|
return content
|
|
884
891
|
.filter((block): block is TextContent => block.type === "text")
|
|
885
|
-
.map(
|
|
892
|
+
.map(block => block.text)
|
|
886
893
|
.join(" ");
|
|
887
894
|
}
|
|
888
895
|
|
|
889
|
-
function collectSessionsFromFiles(files: string[], storage: SessionStorage): SessionInfo[] {
|
|
896
|
+
async function collectSessionsFromFiles(files: string[], storage: SessionStorage): Promise<SessionInfo[]> {
|
|
890
897
|
const sessions: SessionInfo[] = [];
|
|
891
898
|
|
|
892
899
|
for (const file of files) {
|
|
893
900
|
try {
|
|
894
|
-
const content = storage.
|
|
901
|
+
const content = await storage.readText(file);
|
|
895
902
|
const lines = content.trim().split("\n");
|
|
896
903
|
if (lines.length === 0) continue;
|
|
897
904
|
|
|
@@ -1003,10 +1010,10 @@ export class SessionManager {
|
|
|
1003
1010
|
await this._closePersistWriter();
|
|
1004
1011
|
this.persistError = undefined;
|
|
1005
1012
|
this.persistErrorReported = false;
|
|
1006
|
-
this.sessionFile = resolve(sessionFile);
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
const header = this.fileEntries.find(
|
|
1013
|
+
this.sessionFile = path.resolve(sessionFile);
|
|
1014
|
+
this.fileEntries = await loadEntriesFromFile(this.sessionFile, this.storage);
|
|
1015
|
+
if (this.fileEntries.length > 0) {
|
|
1016
|
+
const header = this.fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1010
1017
|
this.sessionId = header?.id ?? nanoid();
|
|
1011
1018
|
this.sessionTitle = header?.title;
|
|
1012
1019
|
|
|
@@ -1053,7 +1060,7 @@ export class SessionManager {
|
|
|
1053
1060
|
|
|
1054
1061
|
if (this.persist) {
|
|
1055
1062
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1056
|
-
this.sessionFile = join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
1063
|
+
this.sessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${this.sessionId}.jsonl`);
|
|
1057
1064
|
}
|
|
1058
1065
|
return this.sessionFile;
|
|
1059
1066
|
}
|
|
@@ -1115,7 +1122,7 @@ export class SessionManager {
|
|
|
1115
1122
|
if (this.persistError && !options?.ignoreError) throw this.persistError;
|
|
1116
1123
|
await task();
|
|
1117
1124
|
});
|
|
1118
|
-
this.persistChain = next.catch(
|
|
1125
|
+
this.persistChain = next.catch(err => {
|
|
1119
1126
|
this._recordPersistError(err);
|
|
1120
1127
|
});
|
|
1121
1128
|
return next;
|
|
@@ -1127,7 +1134,7 @@ export class SessionManager {
|
|
|
1127
1134
|
if (this.persistWriter && this.persistWriterPath === this.sessionFile) return this.persistWriter;
|
|
1128
1135
|
// Note: caller must await _closePersistWriter() before calling this if switching files
|
|
1129
1136
|
this.persistWriter = new NdjsonFileWriter(this.storage, this.sessionFile, {
|
|
1130
|
-
onError:
|
|
1137
|
+
onError: err => {
|
|
1131
1138
|
this._recordPersistError(err);
|
|
1132
1139
|
},
|
|
1133
1140
|
});
|
|
@@ -1154,8 +1161,8 @@ export class SessionManager {
|
|
|
1154
1161
|
|
|
1155
1162
|
private async _writeEntriesAtomically(entries: FileEntry[]): Promise<void> {
|
|
1156
1163
|
if (!this.sessionFile) return;
|
|
1157
|
-
const dir = resolve(this.sessionFile, "..");
|
|
1158
|
-
const tempPath = join(dir, `.${basename(this.sessionFile)}.${nanoid(6)}.tmp`);
|
|
1164
|
+
const dir = path.resolve(this.sessionFile, "..");
|
|
1165
|
+
const tempPath = path.join(dir, `.${path.basename(this.sessionFile)}.${nanoid(6)}.tmp`);
|
|
1159
1166
|
const writer = new NdjsonFileWriter(this.storage, tempPath, { flags: "w" });
|
|
1160
1167
|
try {
|
|
1161
1168
|
for (const entry of entries) {
|
|
@@ -1185,7 +1192,7 @@ export class SessionManager {
|
|
|
1185
1192
|
if (!this.persist || !this.sessionFile) return;
|
|
1186
1193
|
await this._queuePersistTask(async () => {
|
|
1187
1194
|
await this._closePersistWriterInternal();
|
|
1188
|
-
const entries = await Promise.all(this.fileEntries.map(
|
|
1195
|
+
const entries = await Promise.all(this.fileEntries.map(entry => prepareEntryForPersistence(entry)));
|
|
1189
1196
|
await this._writeEntriesAtomically(entries);
|
|
1190
1197
|
this.flushed = true;
|
|
1191
1198
|
});
|
|
@@ -1236,7 +1243,7 @@ export class SessionManager {
|
|
|
1236
1243
|
this.sessionTitle = title;
|
|
1237
1244
|
|
|
1238
1245
|
// Update the in-memory header (so first flush includes title)
|
|
1239
|
-
const header = this.fileEntries.find(
|
|
1246
|
+
const header = this.fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1240
1247
|
if (header) {
|
|
1241
1248
|
header.title = title;
|
|
1242
1249
|
}
|
|
@@ -1252,7 +1259,7 @@ export class SessionManager {
|
|
|
1252
1259
|
if (!this.persist || !this.sessionFile) return;
|
|
1253
1260
|
if (this.persistError) throw this.persistError;
|
|
1254
1261
|
|
|
1255
|
-
const hasAssistant = this.fileEntries.some(
|
|
1262
|
+
const hasAssistant = this.fileEntries.some(e => e.type === "message" && e.message.role === "assistant");
|
|
1256
1263
|
if (!hasAssistant) return;
|
|
1257
1264
|
|
|
1258
1265
|
if (!this.flushed) {
|
|
@@ -1260,7 +1267,7 @@ export class SessionManager {
|
|
|
1260
1267
|
void this._queuePersistTask(async () => {
|
|
1261
1268
|
const writer = this._ensurePersistWriter();
|
|
1262
1269
|
if (!writer) return;
|
|
1263
|
-
const entries = await Promise.all(this.fileEntries.map(
|
|
1270
|
+
const entries = await Promise.all(this.fileEntries.map(e => prepareEntryForPersistence(e)));
|
|
1264
1271
|
for (const persistedEntry of entries) {
|
|
1265
1272
|
await writer.write(persistedEntry);
|
|
1266
1273
|
}
|
|
@@ -1596,7 +1603,7 @@ export class SessionManager {
|
|
|
1596
1603
|
* Get session header.
|
|
1597
1604
|
*/
|
|
1598
1605
|
getHeader(): SessionHeader | null {
|
|
1599
|
-
const h = this.fileEntries.find(
|
|
1606
|
+
const h = this.fileEntries.find(e => e.type === "session");
|
|
1600
1607
|
return h ? (h as SessionHeader) : null;
|
|
1601
1608
|
}
|
|
1602
1609
|
|
|
@@ -1709,18 +1716,18 @@ export class SessionManager {
|
|
|
1709
1716
|
* Returns the new session file path, or undefined if not persisting.
|
|
1710
1717
|
*/
|
|
1711
1718
|
createBranchedSession(leafId: string): string | undefined {
|
|
1712
|
-
const
|
|
1713
|
-
if (
|
|
1719
|
+
const branchPath = this.getBranch(leafId);
|
|
1720
|
+
if (branchPath.length === 0) {
|
|
1714
1721
|
throw new Error(`Entry ${leafId} not found`);
|
|
1715
1722
|
}
|
|
1716
1723
|
|
|
1717
1724
|
// Filter out LabelEntry from path - we'll recreate them from the resolved map
|
|
1718
|
-
const pathWithoutLabels =
|
|
1725
|
+
const pathWithoutLabels = branchPath.filter(e => e.type !== "label");
|
|
1719
1726
|
|
|
1720
1727
|
const newSessionId = nanoid();
|
|
1721
1728
|
const timestamp = new Date().toISOString();
|
|
1722
1729
|
const fileTimestamp = timestamp.replace(/[:.]/g, "-");
|
|
1723
|
-
const newSessionFile = join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
1730
|
+
const newSessionFile = path.join(this.getSessionDir(), `${fileTimestamp}_${newSessionId}.jsonl`);
|
|
1724
1731
|
|
|
1725
1732
|
const header: SessionHeader = {
|
|
1726
1733
|
type: "session",
|
|
@@ -1732,7 +1739,7 @@ export class SessionManager {
|
|
|
1732
1739
|
};
|
|
1733
1740
|
|
|
1734
1741
|
// Collect labels for entries in the path
|
|
1735
|
-
const pathEntryIds = new Set(pathWithoutLabels.map(
|
|
1742
|
+
const pathEntryIds = new Set(pathWithoutLabels.map(e => e.id));
|
|
1736
1743
|
const labelsToWrite: Array<{ targetId: string; label: string }> = [];
|
|
1737
1744
|
for (const [targetId, label] of this.labelsById) {
|
|
1738
1745
|
if (pathEntryIds.has(targetId)) {
|
|
@@ -1777,7 +1784,7 @@ export class SessionManager {
|
|
|
1777
1784
|
for (const { targetId, label } of labelsToWrite) {
|
|
1778
1785
|
const labelEntry: LabelEntry = {
|
|
1779
1786
|
type: "label",
|
|
1780
|
-
id: generateId(new Set([...pathEntryIds, ...labelEntries.map(
|
|
1787
|
+
id: generateId(new Set([...pathEntryIds, ...labelEntries.map(e => e.id)])),
|
|
1781
1788
|
parentId,
|
|
1782
1789
|
timestamp: new Date().toISOString(),
|
|
1783
1790
|
targetId,
|
|
@@ -1816,10 +1823,10 @@ export class SessionManager {
|
|
|
1816
1823
|
): Promise<SessionManager> {
|
|
1817
1824
|
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
1818
1825
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1819
|
-
const forkEntries = structuredClone(loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
1826
|
+
const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
|
|
1820
1827
|
migrateToCurrentVersion(forkEntries);
|
|
1821
|
-
const sourceHeader = forkEntries.find(
|
|
1822
|
-
const historyEntries = forkEntries.filter(
|
|
1828
|
+
const sourceHeader = forkEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1829
|
+
const historyEntries = forkEntries.filter(entry => entry.type !== "session") as SessionEntry[];
|
|
1823
1830
|
manager._newSessionSync({ parentSession: sourceHeader?.id });
|
|
1824
1831
|
const newHeader = manager.fileEntries[0] as SessionHeader;
|
|
1825
1832
|
newHeader.title = sourceHeader?.title;
|
|
@@ -1836,18 +1843,18 @@ export class SessionManager {
|
|
|
1836
1843
|
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
|
|
1837
1844
|
*/
|
|
1838
1845
|
static async open(
|
|
1839
|
-
|
|
1846
|
+
filePath: string,
|
|
1840
1847
|
sessionDir?: string,
|
|
1841
1848
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1842
1849
|
): Promise<SessionManager> {
|
|
1843
1850
|
// Extract cwd from session header if possible, otherwise use process.cwd()
|
|
1844
|
-
const entries = loadEntriesFromFile(
|
|
1845
|
-
const header = entries.find(
|
|
1851
|
+
const entries = await loadEntriesFromFile(filePath, storage);
|
|
1852
|
+
const header = entries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
1846
1853
|
const cwd = header?.cwd ?? process.cwd();
|
|
1847
1854
|
// If no sessionDir provided, derive from file's parent directory
|
|
1848
|
-
const dir = sessionDir ?? resolve(
|
|
1855
|
+
const dir = sessionDir ?? path.resolve(filePath, "..");
|
|
1849
1856
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1850
|
-
await manager._initSessionFile(
|
|
1857
|
+
await manager._initSessionFile(filePath);
|
|
1851
1858
|
return manager;
|
|
1852
1859
|
}
|
|
1853
1860
|
|
|
@@ -1862,7 +1869,7 @@ export class SessionManager {
|
|
|
1862
1869
|
storage: SessionStorage = new FileSessionStorage(),
|
|
1863
1870
|
): Promise<SessionManager> {
|
|
1864
1871
|
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
1865
|
-
const mostRecent = findMostRecentSession(dir, storage);
|
|
1872
|
+
const mostRecent = await findMostRecentSession(dir, storage);
|
|
1866
1873
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
1867
1874
|
if (mostRecent) {
|
|
1868
1875
|
await manager._initSessionFile(mostRecent);
|
|
@@ -1884,11 +1891,15 @@ export class SessionManager {
|
|
|
1884
1891
|
* @param cwd Working directory (used to compute default session directory)
|
|
1885
1892
|
* @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
|
|
1886
1893
|
*/
|
|
1887
|
-
static list(
|
|
1894
|
+
static async list(
|
|
1895
|
+
cwd: string,
|
|
1896
|
+
sessionDir?: string,
|
|
1897
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
1898
|
+
): Promise<SessionInfo[]> {
|
|
1888
1899
|
const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
|
|
1889
1900
|
try {
|
|
1890
1901
|
const files = storage.listFilesSync(dir, "*.jsonl");
|
|
1891
|
-
return collectSessionsFromFiles(files, storage);
|
|
1902
|
+
return await collectSessionsFromFiles(files, storage);
|
|
1892
1903
|
} catch {
|
|
1893
1904
|
return [];
|
|
1894
1905
|
}
|
|
@@ -1897,13 +1908,13 @@ export class SessionManager {
|
|
|
1897
1908
|
/**
|
|
1898
1909
|
* List all sessions across all project directories.
|
|
1899
1910
|
*/
|
|
1900
|
-
static listAll(storage: SessionStorage = new FileSessionStorage()): SessionInfo[] {
|
|
1901
|
-
const sessionsRoot = join(getDefaultAgentDir(), "sessions");
|
|
1911
|
+
static async listAll(storage: SessionStorage = new FileSessionStorage()): Promise<SessionInfo[]> {
|
|
1912
|
+
const sessionsRoot = path.join(getDefaultAgentDir(), "sessions");
|
|
1902
1913
|
try {
|
|
1903
|
-
const files = Array.from(new Bun.Glob("**/*.jsonl").scanSync(sessionsRoot)).map(
|
|
1904
|
-
join(sessionsRoot, name),
|
|
1914
|
+
const files = Array.from(new Bun.Glob("**/*.jsonl").scanSync(sessionsRoot)).map(name =>
|
|
1915
|
+
path.join(sessionsRoot, name),
|
|
1905
1916
|
);
|
|
1906
|
-
return collectSessionsFromFiles(files, storage);
|
|
1917
|
+
return await collectSessionsFromFiles(files, storage);
|
|
1907
1918
|
} catch {
|
|
1908
1919
|
return [];
|
|
1909
1920
|
}
|
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
fsyncSync,
|
|
5
|
-
mkdirSync,
|
|
6
|
-
openSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
readSync,
|
|
9
|
-
statSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
writeSync,
|
|
12
|
-
} from "node:fs";
|
|
13
|
-
import { rename as renameAsync } from "node:fs/promises";
|
|
14
|
-
import { dirname, join } from "node:path";
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
15
4
|
|
|
16
5
|
export interface SessionStorageStat {
|
|
17
6
|
size: number;
|
|
@@ -30,14 +19,13 @@ export interface SessionStorageWriter {
|
|
|
30
19
|
export interface SessionStorage {
|
|
31
20
|
ensureDirSync(dir: string): void;
|
|
32
21
|
existsSync(path: string): boolean;
|
|
33
|
-
readTextSync(path: string): string;
|
|
34
|
-
readTextPrefixSync(path: string, buf: Buffer): number;
|
|
35
22
|
writeTextSync(path: string, content: string): void;
|
|
36
23
|
statSync(path: string): SessionStorageStat;
|
|
37
24
|
listFilesSync(dir: string, pattern: string): string[];
|
|
38
25
|
|
|
39
26
|
exists(path: string): Promise<boolean>;
|
|
40
27
|
readText(path: string): Promise<string>;
|
|
28
|
+
readTextPrefix(path: string, maxBytes: number): Promise<string>;
|
|
41
29
|
writeText(path: string, content: string): Promise<void>;
|
|
42
30
|
rename(path: string, nextPath: string): Promise<void>;
|
|
43
31
|
unlink(path: string): Promise<void>;
|
|
@@ -50,9 +38,9 @@ function toError(value: unknown): Error {
|
|
|
50
38
|
}
|
|
51
39
|
|
|
52
40
|
// FinalizationRegistry to clean up leaked file descriptors
|
|
53
|
-
const writerRegistry = new FinalizationRegistry<number>(
|
|
41
|
+
const writerRegistry = new FinalizationRegistry<number>(fd => {
|
|
54
42
|
try {
|
|
55
|
-
closeSync(fd);
|
|
43
|
+
fs.closeSync(fd);
|
|
56
44
|
} catch {
|
|
57
45
|
// Ignore - fd may already be closed or invalid
|
|
58
46
|
}
|
|
@@ -64,16 +52,16 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
64
52
|
private error: Error | undefined;
|
|
65
53
|
private onError: ((err: Error) => void) | undefined;
|
|
66
54
|
|
|
67
|
-
constructor(
|
|
55
|
+
constructor(fpath: string, options?: { flags?: "a" | "w"; onError?: (err: Error) => void }) {
|
|
68
56
|
this.onError = options?.onError;
|
|
69
57
|
const flags = options?.flags ?? "a";
|
|
70
58
|
// Ensure parent directory exists
|
|
71
|
-
const dir = dirname(
|
|
72
|
-
if (!existsSync(dir)) {
|
|
73
|
-
mkdirSync(dir, { recursive: true });
|
|
59
|
+
const dir = path.dirname(fpath);
|
|
60
|
+
if (!fs.existsSync(dir)) {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
62
|
}
|
|
75
63
|
// Open file once, keep fd for lifetime
|
|
76
|
-
this.fd = openSync(
|
|
64
|
+
this.fd = fs.openSync(fpath, flags === "w" ? "w" : "a");
|
|
77
65
|
// Register for cleanup if abandoned without close()
|
|
78
66
|
writerRegistry.register(this, this.fd, this);
|
|
79
67
|
}
|
|
@@ -92,7 +80,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
92
80
|
const buf = Buffer.from(line, "utf-8");
|
|
93
81
|
let offset = 0;
|
|
94
82
|
while (offset < buf.length) {
|
|
95
|
-
const written = writeSync(this.fd, buf, offset, buf.length - offset);
|
|
83
|
+
const written = fs.writeSync(this.fd, buf, offset, buf.length - offset);
|
|
96
84
|
if (written === 0) {
|
|
97
85
|
throw new Error("Short write");
|
|
98
86
|
}
|
|
@@ -112,7 +100,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
112
100
|
if (this.closed) throw new Error("Writer closed");
|
|
113
101
|
if (this.error) throw this.error;
|
|
114
102
|
try {
|
|
115
|
-
fsyncSync(this.fd);
|
|
103
|
+
fs.fsyncSync(this.fd);
|
|
116
104
|
} catch (err) {
|
|
117
105
|
throw this.recordError(err);
|
|
118
106
|
}
|
|
@@ -124,7 +112,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
124
112
|
// Unregister from finalization - we're closing properly
|
|
125
113
|
writerRegistry.unregister(this);
|
|
126
114
|
try {
|
|
127
|
-
closeSync(this.fd);
|
|
115
|
+
fs.closeSync(this.fd);
|
|
128
116
|
} catch {
|
|
129
117
|
// Ignore close errors
|
|
130
118
|
}
|
|
@@ -137,53 +125,56 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
137
125
|
|
|
138
126
|
export class FileSessionStorage implements SessionStorage {
|
|
139
127
|
ensureDirSync(dir: string): void {
|
|
140
|
-
if (!existsSync(dir)) {
|
|
141
|
-
mkdirSync(dir, { recursive: true });
|
|
128
|
+
if (!fs.existsSync(dir)) {
|
|
129
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
142
130
|
}
|
|
143
131
|
}
|
|
144
132
|
|
|
145
133
|
existsSync(path: string): boolean {
|
|
146
|
-
return existsSync(path);
|
|
134
|
+
return fs.existsSync(path);
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
readTextPrefixSync(path: string, buf: Buffer): number {
|
|
154
|
-
const fd = openSync(path, "r");
|
|
155
|
-
try {
|
|
156
|
-
const bytesRead = readSync(fd, buf, 0, buf.length, 0);
|
|
157
|
-
return bytesRead;
|
|
158
|
-
} finally {
|
|
159
|
-
closeSync(fd);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
writeTextSync(path: string, content: string): void {
|
|
164
|
-
this.ensureDirSync(dirname(path));
|
|
165
|
-
writeFileSync(path, content);
|
|
137
|
+
writeTextSync(fpath: string, content: string): void {
|
|
138
|
+
this.ensureDirSync(path.dirname(fpath));
|
|
139
|
+
fs.writeFileSync(fpath, content);
|
|
166
140
|
}
|
|
167
141
|
|
|
168
142
|
statSync(path: string): SessionStorageStat {
|
|
169
|
-
const stats = statSync(path);
|
|
143
|
+
const stats = fs.statSync(path);
|
|
170
144
|
return { size: stats.size, mtimeMs: stats.mtimeMs, mtime: stats.mtime };
|
|
171
145
|
}
|
|
172
146
|
|
|
173
147
|
listFilesSync(dir: string, pattern: string): string[] {
|
|
174
148
|
try {
|
|
175
|
-
return Array.from(new Bun.Glob(pattern).scanSync(dir)).map(
|
|
149
|
+
return Array.from(new Bun.Glob(pattern).scanSync(dir)).map(name => path.join(dir, name));
|
|
176
150
|
} catch {
|
|
177
151
|
return [];
|
|
178
152
|
}
|
|
179
153
|
}
|
|
180
154
|
|
|
181
|
-
exists(path: string): Promise<boolean> {
|
|
182
|
-
|
|
155
|
+
async exists(path: string): Promise<boolean> {
|
|
156
|
+
try {
|
|
157
|
+
await fs.promises.access(path);
|
|
158
|
+
return true;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (isEnoent(err)) return false;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
183
163
|
}
|
|
184
164
|
|
|
185
165
|
readText(path: string): Promise<string> {
|
|
186
|
-
return
|
|
166
|
+
return fs.promises.readFile(path, "utf-8");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async readTextPrefix(path: string, maxBytes: number): Promise<string> {
|
|
170
|
+
const handle = await fs.promises.open(path, "r");
|
|
171
|
+
try {
|
|
172
|
+
const buffer = Buffer.alloc(maxBytes);
|
|
173
|
+
const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
|
|
174
|
+
return buffer.subarray(0, bytesRead).toString("utf-8");
|
|
175
|
+
} finally {
|
|
176
|
+
await handle.close();
|
|
177
|
+
}
|
|
187
178
|
}
|
|
188
179
|
|
|
189
180
|
async writeText(path: string, content: string): Promise<void> {
|
|
@@ -192,23 +183,23 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
192
183
|
|
|
193
184
|
async rename(path: string, nextPath: string): Promise<void> {
|
|
194
185
|
try {
|
|
195
|
-
await
|
|
186
|
+
await fs.promises.rename(path, nextPath);
|
|
196
187
|
} catch (err) {
|
|
197
188
|
throw toError(err);
|
|
198
189
|
}
|
|
199
190
|
}
|
|
200
191
|
|
|
201
192
|
unlink(path: string): Promise<void> {
|
|
202
|
-
return
|
|
193
|
+
return fs.promises.unlink(path);
|
|
203
194
|
}
|
|
204
195
|
|
|
205
196
|
fsyncDirSync(dir: string): void {
|
|
206
197
|
try {
|
|
207
|
-
const fd = openSync(dir, "r");
|
|
198
|
+
const fd = fs.openSync(dir, "r");
|
|
208
199
|
try {
|
|
209
|
-
fsyncSync(fd);
|
|
200
|
+
fs.fsyncSync(fd);
|
|
210
201
|
} finally {
|
|
211
|
-
closeSync(fd);
|
|
202
|
+
fs.closeSync(fd);
|
|
212
203
|
}
|
|
213
204
|
} catch {
|
|
214
205
|
// Best-effort: some platforms/filesystems don't support fsync on directories.
|
|
@@ -265,7 +256,7 @@ class MemorySessionStorageWriter implements SessionStorageWriter {
|
|
|
265
256
|
await this.ready;
|
|
266
257
|
if (this.error) throw this.error;
|
|
267
258
|
try {
|
|
268
|
-
const existing = this.storage.existsSync(this.path) ? this.storage.
|
|
259
|
+
const existing = this.storage.existsSync(this.path) ? await this.storage.readText(this.path) : "";
|
|
269
260
|
await this.storage.writeText(this.path, `${existing}${line}`);
|
|
270
261
|
} catch (err) {
|
|
271
262
|
throw this.recordError(err);
|
|
@@ -305,17 +296,6 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
305
296
|
return this.files.has(path);
|
|
306
297
|
}
|
|
307
298
|
|
|
308
|
-
readTextSync(path: string): string {
|
|
309
|
-
const entry = this.files.get(path);
|
|
310
|
-
if (!entry) throw new Error(`File not found: ${path}`);
|
|
311
|
-
return entry.content;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
readTextPrefixSync(path: string, buf: Buffer): number {
|
|
315
|
-
const content = this.readTextSync(path);
|
|
316
|
-
return buf.write(content, 0, buf.length, "utf-8");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
299
|
writeTextSync(path: string, content: string): void {
|
|
320
300
|
this.files.set(path, { content, mtimeMs: Date.now() });
|
|
321
301
|
}
|
|
@@ -348,7 +328,15 @@ export class MemorySessionStorage implements SessionStorage {
|
|
|
348
328
|
}
|
|
349
329
|
|
|
350
330
|
readText(path: string): Promise<string> {
|
|
351
|
-
|
|
331
|
+
const entry = this.files.get(path);
|
|
332
|
+
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
|
|
333
|
+
return Promise.resolve(entry.content);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
readTextPrefix(path: string, maxBytes: number): Promise<string> {
|
|
337
|
+
const entry = this.files.get(path);
|
|
338
|
+
if (!entry) return Promise.reject(new Error(`File not found: ${path}`));
|
|
339
|
+
return Promise.resolve(entry.content.slice(0, maxBytes));
|
|
352
340
|
}
|
|
353
341
|
|
|
354
342
|
writeText(path: string, content: string): Promise<void> {
|