@vandeepunk/pi-coding-agent 0.0.6 → 1.0.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 +814 -16
- package/README.md +66 -27
- package/dist/bun/cli.d.ts +3 -0
- package/dist/bun/cli.d.ts.map +1 -0
- package/dist/bun/cli.js +7 -0
- package/dist/bun/cli.js.map +1 -0
- package/dist/bun/register-bedrock.d.ts +2 -0
- package/dist/bun/register-bedrock.d.ts.map +1 -0
- package/dist/bun/register-bedrock.js +4 -0
- package/dist/bun/register-bedrock.js.map +1 -0
- package/dist/cli/args.d.ts +9 -4
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +61 -22
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/file-processor.d.ts.map +1 -1
- package/dist/cli/file-processor.js +4 -0
- package/dist/cli/file-processor.js.map +1 -1
- package/dist/cli/initial-message.d.ts +18 -0
- package/dist/cli/initial-message.d.ts.map +1 -0
- package/dist/cli/initial-message.js +22 -0
- package/dist/cli/initial-message.js.map +1 -0
- package/dist/cli/session-picker.d.ts.map +1 -1
- package/dist/cli/session-picker.js +2 -1
- package/dist/cli/session-picker.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +83 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -0
- package/dist/core/agent-session-runtime.js +236 -0
- package/dist/core/agent-session-runtime.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +86 -0
- package/dist/core/agent-session-services.d.ts.map +1 -0
- package/dist/core/agent-session-services.js +116 -0
- package/dist/core/agent-session-services.js.map +1 -0
- package/dist/core/agent-session.d.ts +63 -49
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +599 -370
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-storage.d.ts +38 -8
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +220 -96
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts +6 -7
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +27 -114
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/branch-summarization.d.ts +2 -0
- package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
- package/dist/core/compaction/branch-summarization.js +3 -2
- package/dist/core/compaction/branch-summarization.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +3 -3
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +31 -25
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/utils.d.ts +3 -0
- package/dist/core/compaction/utils.d.ts.map +1 -1
- package/dist/core/compaction/utils.js +16 -1
- package/dist/core/compaction/utils.js.map +1 -1
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +7 -3
- package/dist/core/exec.js.map +1 -1
- package/dist/core/export-html/index.d.ts +7 -4
- package/dist/core/export-html/index.d.ts.map +1 -1
- package/dist/core/export-html/index.js +10 -8
- package/dist/core/export-html/index.js.map +1 -1
- package/dist/core/export-html/template.css +43 -13
- package/dist/core/export-html/template.html +1 -0
- package/dist/core/export-html/template.js +118 -14
- package/dist/core/export-html/tool-renderer.d.ts +9 -4
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +48 -10
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/index.d.ts +5 -4
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +2 -2
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +49 -13
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +13 -11
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +139 -64
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +174 -34
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +10 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/extensions/wrapper.d.ts +4 -11
- package/dist/core/extensions/wrapper.d.ts.map +1 -1
- package/dist/core/extensions/wrapper.js +6 -86
- package/dist/core/extensions/wrapper.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts +18 -2
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +220 -40
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/index.d.ts +4 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/keybindings.d.ts +283 -50
- package/dist/core/keybindings.d.ts.map +1 -1
- package/dist/core/keybindings.js +221 -134
- package/dist/core/keybindings.js.map +1 -1
- package/dist/core/model-registry.d.ts +33 -3
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +165 -97
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts +35 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +205 -32
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/output-guard.d.ts +6 -0
- package/dist/core/output-guard.d.ts.map +1 -0
- package/dist/core/output-guard.js +59 -0
- package/dist/core/output-guard.js.map +1 -0
- package/dist/core/package-manager.d.ts +43 -2
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +541 -102
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/prompt-templates.d.ts +5 -4
- package/dist/core/prompt-templates.d.ts.map +1 -1
- package/dist/core/prompt-templates.js +35 -37
- package/dist/core/prompt-templates.js.map +1 -1
- package/dist/core/resolve-config-value.d.ts +6 -0
- package/dist/core/resolve-config-value.d.ts.map +1 -1
- package/dist/core/resolve-config-value.js +75 -8
- package/dist/core/resolve-config-value.js.map +1 -1
- package/dist/core/resource-loader.d.ts +6 -5
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +166 -119
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +11 -8
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +31 -29
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-cwd.d.ts +19 -0
- package/dist/core/session-cwd.d.ts.map +1 -0
- package/dist/core/session-cwd.js +38 -0
- package/dist/core/session-cwd.js.map +1 -0
- package/dist/core/session-manager.d.ts +11 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +39 -25
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +60 -10
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +291 -140
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts +5 -3
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +54 -9
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts +2 -3
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +3 -2
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/source-info.d.ts +18 -0
- package/dist/core/source-info.d.ts.map +1 -0
- package/dist/core/source-info.js +19 -0
- package/dist/core/source-info.js.map +1 -0
- package/dist/core/system-prompt.d.ts +4 -0
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +31 -52
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/timings.d.ts +1 -0
- package/dist/core/timings.d.ts.map +1 -1
- package/dist/core/timings.js +6 -0
- package/dist/core/timings.js.map +1 -1
- package/dist/core/tools/bash.d.ts +24 -6
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +225 -115
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +23 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +151 -57
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +20 -6
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +111 -61
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts +6 -0
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -0
- package/dist/core/tools/file-mutation-queue.js +37 -0
- package/dist/core/tools/file-mutation-queue.js.map +1 -0
- package/dist/core/tools/find.d.ts +11 -4
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +82 -30
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts +15 -4
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +83 -29
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +63 -21
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +51 -26
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/ls.d.ts +9 -3
- package/dist/core/tools/ls.d.ts.map +1 -1
- package/dist/core/tools/ls.js +67 -13
- package/dist/core/tools/ls.js.map +1 -1
- package/dist/core/tools/read.d.ts +10 -3
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +110 -51
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +21 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -0
- package/dist/core/tools/render-utils.js +49 -0
- package/dist/core/tools/render-utils.js.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts +14 -0
- package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -0
- package/dist/core/tools/tool-definition-wrapper.js +32 -0
- package/dist/core/tools/tool-definition-wrapper.js.map +1 -0
- package/dist/core/tools/write.d.ts +9 -3
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +168 -30
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +354 -379
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +31 -11
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +3 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +14 -3
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/bash-execution.d.ts +0 -1
- package/dist/modes/interactive/components/bash-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/bash-execution.js +22 -9
- package/dist/modes/interactive/components/bash-execution.js.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
- package/dist/modes/interactive/components/bordered-loader.js +1 -1
- package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/branch-summary-message.js +2 -2
- package/dist/modes/interactive/components/branch-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/compaction-summary-message.js +2 -2
- package/dist/modes/interactive/components/compaction-summary-message.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +14 -14
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/custom-editor.d.ts +3 -3
- package/dist/modes/interactive/components/custom-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/custom-editor.js +6 -6
- package/dist/modes/interactive/components/custom-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-editor.d.ts +5 -2
- package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-editor.js +18 -9
- package/dist/modes/interactive/components/extension-editor.js.map +1 -1
- package/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-input.js +5 -5
- package/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +8 -8
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts +1 -0
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +21 -40
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/index.d.ts +1 -1
- package/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/dist/modes/interactive/components/index.js +1 -1
- package/dist/modes/interactive/components/index.js.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.d.ts +3 -36
- package/dist/modes/interactive/components/keybinding-hints.d.ts.map +1 -1
- package/dist/modes/interactive/components/keybinding-hints.js +5 -44
- package/dist/modes/interactive/components/keybinding-hints.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +7 -7
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +13 -9
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +7 -7
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +4 -4
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +33 -36
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +25 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/show-images-selector.js +5 -1
- package/dist/modes/interactive/components/show-images-selector.js.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/skill-invocation-message.js +2 -2
- package/dist/modes/interactive/components/skill-invocation-message.js.map +1 -1
- package/dist/modes/interactive/components/theme-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/theme-selector.js +5 -1
- package/dist/modes/interactive/components/theme-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +5 -1
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +17 -29
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +139 -501
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/tree-selector.d.ts +25 -4
- package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/tree-selector.js +184 -34
- package/dist/modes/interactive/components/tree-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message-selector.js +6 -6
- package/dist/modes/interactive/components/user-message-selector.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +12 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +25 -17
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +669 -385
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +3 -0
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +83 -48
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/dist/modes/print-mode.d.ts +2 -2
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +90 -79
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/jsonl.d.ts +17 -0
- package/dist/modes/rpc/jsonl.d.ts.map +1 -0
- package/dist/modes/rpc/jsonl.js +49 -0
- package/dist/modes/rpc/jsonl.js.map +1 -0
- package/dist/modes/rpc/rpc-client.d.ts +1 -1
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +8 -11
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +2 -2
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +130 -87
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -4
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts +4 -0
- package/dist/package-manager-cli.d.ts.map +1 -0
- package/dist/package-manager-cli.js +234 -0
- package/dist/package-manager-cli.js.map +1 -0
- package/dist/utils/child-process.d.ts +11 -0
- package/dist/utils/child-process.d.ts.map +1 -0
- package/dist/utils/child-process.js +78 -0
- package/dist/utils/child-process.js.map +1 -0
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +94 -11
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard-native.d.ts +1 -0
- package/dist/utils/clipboard-native.d.ts.map +1 -1
- package/dist/utils/clipboard-native.js.map +1 -1
- package/dist/utils/clipboard.d.ts +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +27 -16
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/exif-orientation.d.ts +5 -0
- package/dist/utils/exif-orientation.d.ts.map +1 -0
- package/dist/utils/exif-orientation.js +158 -0
- package/dist/utils/exif-orientation.js.map +1 -0
- package/dist/utils/git.d.ts +5 -1
- package/dist/utils/git.d.ts.map +1 -1
- package/dist/utils/git.js +14 -3
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/image-convert.d.ts.map +1 -1
- package/dist/utils/image-convert.js +5 -1
- package/dist/utils/image-convert.js.map +1 -1
- package/dist/utils/image-resize.d.ts +5 -5
- package/dist/utils/image-resize.d.ts.map +1 -1
- package/dist/utils/image-resize.js +51 -95
- package/dist/utils/image-resize.js.map +1 -1
- package/dist/utils/paths.d.ts +7 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +19 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +67 -22
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/compaction.md +6 -2
- package/docs/custom-provider.md +57 -9
- package/docs/development.md +3 -1
- package/docs/extensions.md +437 -67
- package/docs/json.md +5 -2
- package/docs/keybindings.md +108 -107
- package/docs/models.md +50 -2
- package/docs/packages.md +17 -10
- package/docs/prompt-templates.md +6 -6
- package/docs/providers.md +10 -1
- package/docs/rpc.md +78 -18
- package/docs/sdk.md +261 -96
- package/docs/settings.md +28 -3
- package/docs/skills.md +9 -4
- package/docs/terminal-setup.md +39 -3
- package/docs/tmux.md +61 -0
- package/docs/tree.md +15 -3
- package/docs/tui.md +2 -2
- package/examples/extensions/README.md +3 -0
- package/examples/extensions/antigravity-image-gen.ts +12 -7
- package/examples/extensions/built-in-tool-renderer.ts +246 -0
- package/examples/extensions/commands.ts +3 -3
- package/examples/extensions/custom-compaction.ts +17 -4
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/doom-overlay/doom/build.sh +2 -2
- package/examples/extensions/dynamic-tools.ts +74 -0
- package/examples/extensions/handoff.ts +5 -2
- package/examples/extensions/hello.ts +18 -17
- package/examples/extensions/hidden-thinking-label.ts +53 -0
- package/examples/extensions/minimal-mode.ts +14 -14
- package/examples/extensions/overlay-qa-tests.ts +468 -1
- package/examples/extensions/preset.ts +2 -3
- package/examples/extensions/provider-payload.ts +14 -0
- package/examples/extensions/qna.ts +5 -2
- package/examples/extensions/question.ts +2 -2
- package/examples/extensions/questionnaire.ts +2 -2
- package/examples/extensions/rpc-demo.ts +3 -9
- package/examples/extensions/sandbox/index.ts +6 -3
- package/examples/extensions/status-line.ts +0 -8
- package/examples/extensions/subagent/README.md +4 -4
- package/examples/extensions/subagent/agents.ts +2 -3
- package/examples/extensions/subagent/index.ts +30 -8
- package/examples/extensions/summarize.ts +15 -4
- package/examples/extensions/todo.ts +2 -4
- package/examples/extensions/tool-override.ts +10 -9
- package/examples/extensions/tools.ts +0 -5
- package/examples/extensions/trigger-compact.ts +11 -1
- package/examples/extensions/truncated-tool.ts +8 -5
- package/examples/extensions/widget-placement.ts +4 -12
- package/examples/extensions/with-deps/index.ts +1 -5
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/02-custom-model.ts +2 -2
- package/examples/sdk/04-skills.ts +8 -2
- package/examples/sdk/08-prompt-templates.ts +4 -3
- package/examples/sdk/09-api-keys-and-oauth.ts +5 -5
- package/examples/sdk/10-settings.ts +13 -0
- package/examples/sdk/12-full-control.ts +2 -3
- package/examples/sdk/13-session-runtime.ts +67 -0
- package/examples/sdk/README.md +10 -7
- package/package.json +98 -94
- /package/examples/extensions/subagent/{commands → prompts}/implement-and-review.md +0 -0
- /package/examples/extensions/subagent/{commands → prompts}/implement.md +0 -0
- /package/examples/extensions/subagent/{commands → prompts}/scout-and-plan.md +0 -0
|
@@ -15,15 +15,42 @@ export type OAuthCredential = {
|
|
|
15
15
|
} & OAuthCredentials;
|
|
16
16
|
export type AuthCredential = ApiKeyCredential | OAuthCredential;
|
|
17
17
|
export type AuthStorageData = Record<string, AuthCredential>;
|
|
18
|
+
type LockResult<T> = {
|
|
19
|
+
result: T;
|
|
20
|
+
next?: string;
|
|
21
|
+
};
|
|
22
|
+
export interface AuthStorageBackend {
|
|
23
|
+
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
|
|
24
|
+
withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
|
|
25
|
+
}
|
|
26
|
+
export declare class FileAuthStorageBackend implements AuthStorageBackend {
|
|
27
|
+
private authPath;
|
|
28
|
+
constructor(authPath?: string);
|
|
29
|
+
private ensureParentDir;
|
|
30
|
+
private ensureFileExists;
|
|
31
|
+
private acquireLockSyncWithRetry;
|
|
32
|
+
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
|
|
33
|
+
withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
|
|
34
|
+
}
|
|
35
|
+
export declare class InMemoryAuthStorageBackend implements AuthStorageBackend {
|
|
36
|
+
private value;
|
|
37
|
+
withLock<T>(fn: (current: string | undefined) => LockResult<T>): T;
|
|
38
|
+
withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;
|
|
39
|
+
}
|
|
18
40
|
/**
|
|
19
41
|
* Credential storage backed by a JSON file.
|
|
20
42
|
*/
|
|
21
43
|
export declare class AuthStorage {
|
|
22
|
-
private
|
|
44
|
+
private storage;
|
|
23
45
|
private data;
|
|
24
46
|
private runtimeOverrides;
|
|
25
47
|
private fallbackResolver?;
|
|
26
|
-
|
|
48
|
+
private loadError;
|
|
49
|
+
private errors;
|
|
50
|
+
private constructor();
|
|
51
|
+
static create(authPath?: string): AuthStorage;
|
|
52
|
+
static fromStorage(storage: AuthStorageBackend): AuthStorage;
|
|
53
|
+
static inMemory(data?: AuthStorageData): AuthStorage;
|
|
27
54
|
/**
|
|
28
55
|
* Set a runtime API key override (not persisted to disk).
|
|
29
56
|
* Used for CLI --api-key flag.
|
|
@@ -38,14 +65,13 @@ export declare class AuthStorage {
|
|
|
38
65
|
* Used for custom provider keys from models.json.
|
|
39
66
|
*/
|
|
40
67
|
setFallbackResolver(resolver: (provider: string) => string | undefined): void;
|
|
68
|
+
private recordError;
|
|
69
|
+
private parseStorageData;
|
|
41
70
|
/**
|
|
42
|
-
* Reload credentials from
|
|
71
|
+
* Reload credentials from storage.
|
|
43
72
|
*/
|
|
44
73
|
reload(): void;
|
|
45
|
-
|
|
46
|
-
* Save credentials to disk.
|
|
47
|
-
*/
|
|
48
|
-
private save;
|
|
74
|
+
private persistProviderChange;
|
|
49
75
|
/**
|
|
50
76
|
* Get credential for a provider.
|
|
51
77
|
*/
|
|
@@ -75,6 +101,7 @@ export declare class AuthStorage {
|
|
|
75
101
|
* Get all credentials (for passing to getOAuthApiKey).
|
|
76
102
|
*/
|
|
77
103
|
getAll(): AuthStorageData;
|
|
104
|
+
drainErrors(): Error[];
|
|
78
105
|
/**
|
|
79
106
|
* Login to an OAuth provider.
|
|
80
107
|
*/
|
|
@@ -93,10 +120,13 @@ export declare class AuthStorage {
|
|
|
93
120
|
* 4. Environment variable
|
|
94
121
|
* 5. Fallback resolver (models.json custom providers)
|
|
95
122
|
*/
|
|
96
|
-
getApiKey(providerId: string
|
|
123
|
+
getApiKey(providerId: string, options?: {
|
|
124
|
+
includeFallback?: boolean;
|
|
125
|
+
}): Promise<string | undefined>;
|
|
97
126
|
/**
|
|
98
127
|
* Get all registered OAuth providers
|
|
99
128
|
*/
|
|
100
129
|
getOAuthProviders(): import("@mariozechner/pi-ai").OAuthProviderInterface[];
|
|
101
130
|
}
|
|
131
|
+
export {};
|
|
102
132
|
//# sourceMappingURL=auth-storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAKN,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EACxB,KAAK,eAAe,EACpB,MAAM,qBAAqB,CAAC;AAO7B,MAAM,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;CACZ,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC7B,IAAI,EAAE,OAAO,CAAC;CACd,GAAG,gBAAgB,CAAC;AAErB,MAAM,MAAM,cAAc,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAEhE,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;AAE7D;;GAEG;AACH,qBAAa,WAAW;IAKX,OAAO,CAAC,QAAQ;IAJ5B,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,gBAAgB,CAAC,CAA2C;IAEpE,YAAoB,QAAQ,GAAE,MAAyC,EAEtE;IAED;;;OAGG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEvD;IAED;;OAEG;IACH,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1C;IAED;;;OAGG;IACH,mBAAmB,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GAAG,IAAI,CAE5E;IAED;;OAEG;IACH,MAAM,IAAI,IAAI,CAUb;IAED;;OAEG;IACH,OAAO,CAAC,IAAI;IASZ;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAEhD;IAED;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,cAAc,GAAG,IAAI,CAGtD;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAG7B;IAED;;OAEG;IACH,IAAI,IAAI,MAAM,EAAE,CAEf;IAED;;OAEG;IACH,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE7B;IAED;;;OAGG;IACH,OAAO,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMjC;IAED;;OAEG;IACH,MAAM,IAAI,eAAe,CAExB;IAED;;OAEG;IACG,KAAK,CAAC,UAAU,EAAE,eAAe,EAAE,SAAS,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQtF;IAED;;OAEG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE7B;YAOa,yBAAyB;IA+FvC;;;;;;;;OAQG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAwD/D;IAED;;OAEG;IACH,iBAAiB,2DAEhB;CACD","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n *\n * Uses file locking to prevent race conditions when multiple pi instances\n * try to refresh tokens simultaneously.\n */\n\nimport {\n\tgetEnvApiKey,\n\tgetOAuthApiKey,\n\tgetOAuthProvider,\n\tgetOAuthProviders,\n\ttype OAuthCredentials,\n\ttype OAuthLoginCallbacks,\n\ttype OAuthProviderId,\n} from \"@mariozechner/pi-ai\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.js\";\nimport { resolveConfigValue } from \"./resolve-config-value.js\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\n\tconstructor(private authPath: string = join(getAgentDir(), \"auth.json\")) {\n\t\tthis.reload();\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\t/**\n\t * Reload credentials from disk.\n\t */\n\treload(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tthis.data = {};\n\t\t\treturn;\n\t\t}\n\t\ttry {\n\t\t\tthis.data = JSON.parse(readFileSync(this.authPath, \"utf-8\"));\n\t\t} catch {\n\t\t\tthis.data = {};\n\t\t}\n\t}\n\n\t/**\n\t * Save credentials to disk.\n\t */\n\tprivate save(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t\twriteFileSync(this.authPath, JSON.stringify(this.data, null, 2), \"utf-8\");\n\t\tchmodSync(this.authPath, 0o600);\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.save();\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.save();\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(providerId: OAuthProviderId, callbacks: OAuthLoginCallbacks): Promise<void> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\tthrow new Error(`Unknown OAuth provider: ${providerId}`);\n\t\t}\n\n\t\tconst credentials = await provider.login(callbacks);\n\t\tthis.set(providerId, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Refresh OAuth token with file locking to prevent race conditions.\n\t * Multiple pi instances may try to refresh simultaneously when tokens expire.\n\t * This ensures only one instance refreshes while others wait and use the result.\n\t */\n\tprivate async refreshOAuthTokenWithLock(\n\t\tproviderId: OAuthProviderId,\n\t): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\treturn null;\n\t\t}\n\n\t\t// Ensure auth file exists for locking\n\t\tif (!existsSync(this.authPath)) {\n\t\t\tconst dir = dirname(this.authPath);\n\t\t\tif (!existsSync(dir)) {\n\t\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t\t}\n\t\t\twriteFileSync(this.authPath, \"{}\", \"utf-8\");\n\t\t\tchmodSync(this.authPath, 0o600);\n\t\t}\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\tlet lockCompromised = false;\n\t\tlet lockCompromisedError: Error | undefined;\n\t\tconst throwIfLockCompromised = () => {\n\t\t\tif (lockCompromised) {\n\t\t\t\tthrow lockCompromisedError ?? new Error(\"OAuth refresh lock was compromised\");\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\t// Acquire exclusive lock with retry and timeout\n\t\t\t// Use generous retry window to handle slow token endpoints\n\t\t\trelease = await lockfile.lock(this.authPath, {\n\t\t\t\tretries: {\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: 100,\n\t\t\t\t\tmaxTimeout: 10000,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tstale: 30000, // Consider lock stale after 30 seconds\n\t\t\t\tonCompromised: (err) => {\n\t\t\t\t\tlockCompromised = true;\n\t\t\t\t\tlockCompromisedError = err;\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tthrowIfLockCompromised();\n\n\t\t\t// Re-read file after acquiring lock - another instance may have refreshed\n\t\t\tthis.reload();\n\n\t\t\tconst cred = this.data[providerId];\n\t\t\tif (cred?.type !== \"oauth\") {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\t// Check if token is still expired after re-reading\n\t\t\t// (another instance may have already refreshed it)\n\t\t\tif (Date.now() < cred.expires) {\n\t\t\t\t// Token is now valid - another instance refreshed it\n\t\t\t\tthrowIfLockCompromised();\n\t\t\t\tconst apiKey = provider.getApiKey(cred);\n\t\t\t\treturn { apiKey, newCredentials: cred };\n\t\t\t}\n\n\t\t\t// Token still expired, we need to refresh\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(this.data)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await getOAuthApiKey(providerId, oauthCreds);\n\t\t\tif (result) {\n\t\t\t\tthrowIfLockCompromised();\n\t\t\t\tthis.data[providerId] = { type: \"oauth\", ...result.newCredentials };\n\t\t\t\tthis.save();\n\t\t\t\tthrowIfLockCompromised();\n\t\t\t\treturn result;\n\t\t\t}\n\n\t\t\tthrowIfLockCompromised();\n\t\t\treturn null;\n\t\t} finally {\n\t\t\t// Always release the lock\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore unlock errors (lock may have been compromised)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed with locking)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(providerId: string): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(providerId);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[providerId];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn resolveConfigValue(cred.key);\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\tconst provider = getOAuthProvider(providerId);\n\t\t\tif (!provider) {\n\t\t\t\t// Unknown OAuth provider, can't get API key\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Check if token needs refresh\n\t\t\tconst needsRefresh = Date.now() >= cred.expires;\n\n\t\t\tif (needsRefresh) {\n\t\t\t\t// Use locked refresh to prevent race conditions\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.refreshOAuthTokenWithLock(providerId);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treturn result.apiKey;\n\t\t\t\t\t}\n\t\t\t\t} catch {\n\t\t\t\t\t// Refresh failed - re-read file to check if another instance succeeded\n\t\t\t\t\tthis.reload();\n\t\t\t\t\tconst updatedCred = this.data[providerId];\n\n\t\t\t\t\tif (updatedCred?.type === \"oauth\" && Date.now() < updatedCred.expires) {\n\t\t\t\t\t\t// Another instance refreshed successfully, use those credentials\n\t\t\t\t\t\treturn provider.getApiKey(updatedCred);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Refresh truly failed - return undefined so model discovery skips this provider\n\t\t\t\t\t// User can /login to re-authenticate (credentials preserved for retry)\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Token not expired, use current access token\n\t\t\t\treturn provider.getApiKey(cred);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(providerId);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\treturn this.fallbackResolver?.(providerId) ?? undefined;\n\t}\n\n\t/**\n\t * Get all registered OAuth providers\n\t */\n\tgetOAuthProviders() {\n\t\treturn getOAuthProviders();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n *\n * Uses file locking to prevent race conditions when multiple pi instances\n * try to refresh tokens simultaneously.\n */\n\nimport {\n\tgetEnvApiKey,\n\ttype OAuthCredentials,\n\ttype OAuthLoginCallbacks,\n\ttype OAuthProviderId,\n} from \"@mariozechner/pi-ai\";\nimport { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from \"@mariozechner/pi-ai/oauth\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.js\";\nimport { resolveConfigValue } from \"./resolve-config-value.js\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\ntype LockResult<T> = {\n\tresult: T;\n\tnext?: string;\n};\n\nexport interface AuthStorageBackend {\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T;\n\twithLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;\n}\n\nexport class FileAuthStorageBackend implements AuthStorageBackend {\n\tconstructor(private authPath: string = join(getAgentDir(), \"auth.json\")) {}\n\n\tprivate ensureParentDir(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t}\n\n\tprivate ensureFileExists(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\twriteFileSync(this.authPath, \"{}\", \"utf-8\");\n\t\t\tchmodSync(this.authPath, 0o600);\n\t\t}\n\t}\n\n\tprivate acquireLockSyncWithRetry(path: string): () => void {\n\t\tconst maxAttempts = 10;\n\t\tconst delayMs = 20;\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\t\ttry {\n\t\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t\t} catch (error) {\n\t\t\t\tconst code =\n\t\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlastError = error;\n\t\t\t\tconst start = Date.now();\n\t\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t\t// Sleep synchronously to avoid changing callers to async.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthrow (lastError as Error) ?? new Error(\"Failed to acquire auth storage lock\");\n\t}\n\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T {\n\t\tthis.ensureParentDir();\n\t\tthis.ensureFileExists();\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = this.acquireLockSyncWithRetry(this.authPath);\n\t\t\tconst current = existsSync(this.authPath) ? readFileSync(this.authPath, \"utf-8\") : undefined;\n\t\t\tconst { result, next } = fn(current);\n\t\t\tif (next !== undefined) {\n\t\t\t\twriteFileSync(this.authPath, next, \"utf-8\");\n\t\t\t\tchmodSync(this.authPath, 0o600);\n\t\t\t}\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\trelease();\n\t\t\t}\n\t\t}\n\t}\n\n\tasync withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T> {\n\t\tthis.ensureParentDir();\n\t\tthis.ensureFileExists();\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\tlet lockCompromised = false;\n\t\tlet lockCompromisedError: Error | undefined;\n\t\tconst throwIfCompromised = () => {\n\t\t\tif (lockCompromised) {\n\t\t\t\tthrow lockCompromisedError ?? new Error(\"Auth storage lock was compromised\");\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\trelease = await lockfile.lock(this.authPath, {\n\t\t\t\tretries: {\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: 100,\n\t\t\t\t\tmaxTimeout: 10000,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tstale: 30000,\n\t\t\t\tonCompromised: (err) => {\n\t\t\t\t\tlockCompromised = true;\n\t\t\t\t\tlockCompromisedError = err;\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tthrowIfCompromised();\n\t\t\tconst current = existsSync(this.authPath) ? readFileSync(this.authPath, \"utf-8\") : undefined;\n\t\t\tconst { result, next } = await fn(current);\n\t\t\tthrowIfCompromised();\n\t\t\tif (next !== undefined) {\n\t\t\t\twriteFileSync(this.authPath, next, \"utf-8\");\n\t\t\t\tchmodSync(this.authPath, 0o600);\n\t\t\t}\n\t\t\tthrowIfCompromised();\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore unlock errors when lock is compromised.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class InMemoryAuthStorageBackend implements AuthStorageBackend {\n\tprivate value: string | undefined;\n\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T {\n\t\tconst { result, next } = fn(this.value);\n\t\tif (next !== undefined) {\n\t\t\tthis.value = next;\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T> {\n\t\tconst { result, next } = await fn(this.value);\n\t\tif (next !== undefined) {\n\t\t\tthis.value = next;\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\tprivate loadError: Error | null = null;\n\tprivate errors: Error[] = [];\n\n\tprivate constructor(private storage: AuthStorageBackend) {\n\t\tthis.reload();\n\t}\n\n\tstatic create(authPath?: string): AuthStorage {\n\t\treturn new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), \"auth.json\")));\n\t}\n\n\tstatic fromStorage(storage: AuthStorageBackend): AuthStorage {\n\t\treturn new AuthStorage(storage);\n\t}\n\n\tstatic inMemory(data: AuthStorageData = {}): AuthStorage {\n\t\tconst storage = new InMemoryAuthStorageBackend();\n\t\tstorage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) }));\n\t\treturn AuthStorage.fromStorage(storage);\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\tprivate recordError(error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push(normalizedError);\n\t}\n\n\tprivate parseStorageData(content: string | undefined): AuthStorageData {\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\treturn JSON.parse(content) as AuthStorageData;\n\t}\n\n\t/**\n\t * Reload credentials from storage.\n\t */\n\treload(): void {\n\t\tlet content: string | undefined;\n\t\ttry {\n\t\t\tthis.storage.withLock((current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\tthis.data = this.parseStorageData(content);\n\t\t\tthis.loadError = null;\n\t\t} catch (error) {\n\t\t\tthis.loadError = error as Error;\n\t\t\tthis.recordError(error);\n\t\t}\n\t}\n\n\tprivate persistProviderChange(provider: string, credential: AuthCredential | undefined): void {\n\t\tif (this.loadError) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.storage.withLock((current) => {\n\t\t\t\tconst currentData = this.parseStorageData(current);\n\t\t\t\tconst merged: AuthStorageData = { ...currentData };\n\t\t\t\tif (credential) {\n\t\t\t\t\tmerged[provider] = credential;\n\t\t\t\t} else {\n\t\t\t\t\tdelete merged[provider];\n\t\t\t\t}\n\t\t\t\treturn { result: undefined, next: JSON.stringify(merged, null, 2) };\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.recordError(error);\n\t\t}\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.persistProviderChange(provider, credential);\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.persistProviderChange(provider, undefined);\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\tdrainErrors(): Error[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(providerId: OAuthProviderId, callbacks: OAuthLoginCallbacks): Promise<void> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\tthrow new Error(`Unknown OAuth provider: ${providerId}`);\n\t\t}\n\n\t\tconst credentials = await provider.login(callbacks);\n\t\tthis.set(providerId, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Refresh OAuth token with backend locking to prevent race conditions.\n\t * Multiple pi instances may try to refresh simultaneously when tokens expire.\n\t */\n\tprivate async refreshOAuthTokenWithLock(\n\t\tproviderId: OAuthProviderId,\n\t): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst result = await this.storage.withLockAsync(async (current) => {\n\t\t\tconst currentData = this.parseStorageData(current);\n\t\t\tthis.data = currentData;\n\t\t\tthis.loadError = null;\n\n\t\t\tconst cred = currentData[providerId];\n\t\t\tif (cred?.type !== \"oauth\") {\n\t\t\t\treturn { result: null };\n\t\t\t}\n\n\t\t\tif (Date.now() < cred.expires) {\n\t\t\t\treturn { result: { apiKey: provider.getApiKey(cred), newCredentials: cred } };\n\t\t\t}\n\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(currentData)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst refreshed = await getOAuthApiKey(providerId, oauthCreds);\n\t\t\tif (!refreshed) {\n\t\t\t\treturn { result: null };\n\t\t\t}\n\n\t\t\tconst merged: AuthStorageData = {\n\t\t\t\t...currentData,\n\t\t\t\t[providerId]: { type: \"oauth\", ...refreshed.newCredentials },\n\t\t\t};\n\t\t\tthis.data = merged;\n\t\t\tthis.loadError = null;\n\t\t\treturn { result: refreshed, next: JSON.stringify(merged, null, 2) };\n\t\t});\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed with locking)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(providerId: string, options?: { includeFallback?: boolean }): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(providerId);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[providerId];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn resolveConfigValue(cred.key);\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\tconst provider = getOAuthProvider(providerId);\n\t\t\tif (!provider) {\n\t\t\t\t// Unknown OAuth provider, can't get API key\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Check if token needs refresh\n\t\t\tconst needsRefresh = Date.now() >= cred.expires;\n\n\t\t\tif (needsRefresh) {\n\t\t\t\t// Use locked refresh to prevent race conditions\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.refreshOAuthTokenWithLock(providerId);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treturn result.apiKey;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.recordError(error);\n\t\t\t\t\t// Refresh failed - re-read file to check if another instance succeeded\n\t\t\t\t\tthis.reload();\n\t\t\t\t\tconst updatedCred = this.data[providerId];\n\n\t\t\t\t\tif (updatedCred?.type === \"oauth\" && Date.now() < updatedCred.expires) {\n\t\t\t\t\t\t// Another instance refreshed successfully, use those credentials\n\t\t\t\t\t\treturn provider.getApiKey(updatedCred);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Refresh truly failed - return undefined so model discovery skips this provider\n\t\t\t\t\t// User can /login to re-authenticate (credentials preserved for retry)\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Token not expired, use current access token\n\t\t\t\treturn provider.getApiKey(cred);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(providerId);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\tif (options?.includeFallback !== false) {\n\t\t\treturn this.fallbackResolver?.(providerId) ?? undefined;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered OAuth providers\n\t */\n\tgetOAuthProviders() {\n\t\treturn getOAuthProviders();\n\t}\n}\n"]}
|
|
@@ -5,24 +5,165 @@
|
|
|
5
5
|
* Uses file locking to prevent race conditions when multiple pi instances
|
|
6
6
|
* try to refresh tokens simultaneously.
|
|
7
7
|
*/
|
|
8
|
-
import { getEnvApiKey,
|
|
8
|
+
import { getEnvApiKey, } from "@mariozechner/pi-ai";
|
|
9
|
+
import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@mariozechner/pi-ai/oauth";
|
|
9
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
11
|
import { dirname, join } from "path";
|
|
11
12
|
import lockfile from "proper-lockfile";
|
|
12
13
|
import { getAgentDir } from "../config.js";
|
|
13
14
|
import { resolveConfigValue } from "./resolve-config-value.js";
|
|
15
|
+
export class FileAuthStorageBackend {
|
|
16
|
+
authPath;
|
|
17
|
+
constructor(authPath = join(getAgentDir(), "auth.json")) {
|
|
18
|
+
this.authPath = authPath;
|
|
19
|
+
}
|
|
20
|
+
ensureParentDir() {
|
|
21
|
+
const dir = dirname(this.authPath);
|
|
22
|
+
if (!existsSync(dir)) {
|
|
23
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
ensureFileExists() {
|
|
27
|
+
if (!existsSync(this.authPath)) {
|
|
28
|
+
writeFileSync(this.authPath, "{}", "utf-8");
|
|
29
|
+
chmodSync(this.authPath, 0o600);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
acquireLockSyncWithRetry(path) {
|
|
33
|
+
const maxAttempts = 10;
|
|
34
|
+
const delayMs = 20;
|
|
35
|
+
let lastError;
|
|
36
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
37
|
+
try {
|
|
38
|
+
return lockfile.lockSync(path, { realpath: false });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const code = typeof error === "object" && error !== null && "code" in error
|
|
42
|
+
? String(error.code)
|
|
43
|
+
: undefined;
|
|
44
|
+
if (code !== "ELOCKED" || attempt === maxAttempts) {
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
lastError = error;
|
|
48
|
+
const start = Date.now();
|
|
49
|
+
while (Date.now() - start < delayMs) {
|
|
50
|
+
// Sleep synchronously to avoid changing callers to async.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw lastError ?? new Error("Failed to acquire auth storage lock");
|
|
55
|
+
}
|
|
56
|
+
withLock(fn) {
|
|
57
|
+
this.ensureParentDir();
|
|
58
|
+
this.ensureFileExists();
|
|
59
|
+
let release;
|
|
60
|
+
try {
|
|
61
|
+
release = this.acquireLockSyncWithRetry(this.authPath);
|
|
62
|
+
const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
|
|
63
|
+
const { result, next } = fn(current);
|
|
64
|
+
if (next !== undefined) {
|
|
65
|
+
writeFileSync(this.authPath, next, "utf-8");
|
|
66
|
+
chmodSync(this.authPath, 0o600);
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (release) {
|
|
72
|
+
release();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async withLockAsync(fn) {
|
|
77
|
+
this.ensureParentDir();
|
|
78
|
+
this.ensureFileExists();
|
|
79
|
+
let release;
|
|
80
|
+
let lockCompromised = false;
|
|
81
|
+
let lockCompromisedError;
|
|
82
|
+
const throwIfCompromised = () => {
|
|
83
|
+
if (lockCompromised) {
|
|
84
|
+
throw lockCompromisedError ?? new Error("Auth storage lock was compromised");
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
try {
|
|
88
|
+
release = await lockfile.lock(this.authPath, {
|
|
89
|
+
retries: {
|
|
90
|
+
retries: 10,
|
|
91
|
+
factor: 2,
|
|
92
|
+
minTimeout: 100,
|
|
93
|
+
maxTimeout: 10000,
|
|
94
|
+
randomize: true,
|
|
95
|
+
},
|
|
96
|
+
stale: 30000,
|
|
97
|
+
onCompromised: (err) => {
|
|
98
|
+
lockCompromised = true;
|
|
99
|
+
lockCompromisedError = err;
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
throwIfCompromised();
|
|
103
|
+
const current = existsSync(this.authPath) ? readFileSync(this.authPath, "utf-8") : undefined;
|
|
104
|
+
const { result, next } = await fn(current);
|
|
105
|
+
throwIfCompromised();
|
|
106
|
+
if (next !== undefined) {
|
|
107
|
+
writeFileSync(this.authPath, next, "utf-8");
|
|
108
|
+
chmodSync(this.authPath, 0o600);
|
|
109
|
+
}
|
|
110
|
+
throwIfCompromised();
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
if (release) {
|
|
115
|
+
try {
|
|
116
|
+
await release();
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Ignore unlock errors when lock is compromised.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export class InMemoryAuthStorageBackend {
|
|
126
|
+
value;
|
|
127
|
+
withLock(fn) {
|
|
128
|
+
const { result, next } = fn(this.value);
|
|
129
|
+
if (next !== undefined) {
|
|
130
|
+
this.value = next;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
async withLockAsync(fn) {
|
|
135
|
+
const { result, next } = await fn(this.value);
|
|
136
|
+
if (next !== undefined) {
|
|
137
|
+
this.value = next;
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
14
142
|
/**
|
|
15
143
|
* Credential storage backed by a JSON file.
|
|
16
144
|
*/
|
|
17
145
|
export class AuthStorage {
|
|
18
|
-
|
|
146
|
+
storage;
|
|
19
147
|
data = {};
|
|
20
148
|
runtimeOverrides = new Map();
|
|
21
149
|
fallbackResolver;
|
|
22
|
-
|
|
23
|
-
|
|
150
|
+
loadError = null;
|
|
151
|
+
errors = [];
|
|
152
|
+
constructor(storage) {
|
|
153
|
+
this.storage = storage;
|
|
24
154
|
this.reload();
|
|
25
155
|
}
|
|
156
|
+
static create(authPath) {
|
|
157
|
+
return new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), "auth.json")));
|
|
158
|
+
}
|
|
159
|
+
static fromStorage(storage) {
|
|
160
|
+
return new AuthStorage(storage);
|
|
161
|
+
}
|
|
162
|
+
static inMemory(data = {}) {
|
|
163
|
+
const storage = new InMemoryAuthStorageBackend();
|
|
164
|
+
storage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) }));
|
|
165
|
+
return AuthStorage.fromStorage(storage);
|
|
166
|
+
}
|
|
26
167
|
/**
|
|
27
168
|
* Set a runtime API key override (not persisted to disk).
|
|
28
169
|
* Used for CLI --api-key flag.
|
|
@@ -43,31 +184,54 @@ export class AuthStorage {
|
|
|
43
184
|
setFallbackResolver(resolver) {
|
|
44
185
|
this.fallbackResolver = resolver;
|
|
45
186
|
}
|
|
187
|
+
recordError(error) {
|
|
188
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
189
|
+
this.errors.push(normalizedError);
|
|
190
|
+
}
|
|
191
|
+
parseStorageData(content) {
|
|
192
|
+
if (!content) {
|
|
193
|
+
return {};
|
|
194
|
+
}
|
|
195
|
+
return JSON.parse(content);
|
|
196
|
+
}
|
|
46
197
|
/**
|
|
47
|
-
* Reload credentials from
|
|
198
|
+
* Reload credentials from storage.
|
|
48
199
|
*/
|
|
49
200
|
reload() {
|
|
50
|
-
|
|
51
|
-
this.data = {};
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
201
|
+
let content;
|
|
54
202
|
try {
|
|
55
|
-
this.
|
|
203
|
+
this.storage.withLock((current) => {
|
|
204
|
+
content = current;
|
|
205
|
+
return { result: undefined };
|
|
206
|
+
});
|
|
207
|
+
this.data = this.parseStorageData(content);
|
|
208
|
+
this.loadError = null;
|
|
56
209
|
}
|
|
57
|
-
catch {
|
|
58
|
-
this.
|
|
210
|
+
catch (error) {
|
|
211
|
+
this.loadError = error;
|
|
212
|
+
this.recordError(error);
|
|
59
213
|
}
|
|
60
214
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
215
|
+
persistProviderChange(provider, credential) {
|
|
216
|
+
if (this.loadError) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
this.storage.withLock((current) => {
|
|
221
|
+
const currentData = this.parseStorageData(current);
|
|
222
|
+
const merged = { ...currentData };
|
|
223
|
+
if (credential) {
|
|
224
|
+
merged[provider] = credential;
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
delete merged[provider];
|
|
228
|
+
}
|
|
229
|
+
return { result: undefined, next: JSON.stringify(merged, null, 2) };
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
this.recordError(error);
|
|
68
234
|
}
|
|
69
|
-
writeFileSync(this.authPath, JSON.stringify(this.data, null, 2), "utf-8");
|
|
70
|
-
chmodSync(this.authPath, 0o600);
|
|
71
235
|
}
|
|
72
236
|
/**
|
|
73
237
|
* Get credential for a provider.
|
|
@@ -80,14 +244,14 @@ export class AuthStorage {
|
|
|
80
244
|
*/
|
|
81
245
|
set(provider, credential) {
|
|
82
246
|
this.data[provider] = credential;
|
|
83
|
-
this.
|
|
247
|
+
this.persistProviderChange(provider, credential);
|
|
84
248
|
}
|
|
85
249
|
/**
|
|
86
250
|
* Remove credential for a provider.
|
|
87
251
|
*/
|
|
88
252
|
remove(provider) {
|
|
89
253
|
delete this.data[provider];
|
|
90
|
-
this.
|
|
254
|
+
this.persistProviderChange(provider, undefined);
|
|
91
255
|
}
|
|
92
256
|
/**
|
|
93
257
|
* List all providers with credentials.
|
|
@@ -122,6 +286,11 @@ export class AuthStorage {
|
|
|
122
286
|
getAll() {
|
|
123
287
|
return { ...this.data };
|
|
124
288
|
}
|
|
289
|
+
drainErrors() {
|
|
290
|
+
const drained = [...this.errors];
|
|
291
|
+
this.errors = [];
|
|
292
|
+
return drained;
|
|
293
|
+
}
|
|
125
294
|
/**
|
|
126
295
|
* Login to an OAuth provider.
|
|
127
296
|
*/
|
|
@@ -140,93 +309,44 @@ export class AuthStorage {
|
|
|
140
309
|
this.remove(provider);
|
|
141
310
|
}
|
|
142
311
|
/**
|
|
143
|
-
* Refresh OAuth token with
|
|
312
|
+
* Refresh OAuth token with backend locking to prevent race conditions.
|
|
144
313
|
* Multiple pi instances may try to refresh simultaneously when tokens expire.
|
|
145
|
-
* This ensures only one instance refreshes while others wait and use the result.
|
|
146
314
|
*/
|
|
147
315
|
async refreshOAuthTokenWithLock(providerId) {
|
|
148
316
|
const provider = getOAuthProvider(providerId);
|
|
149
317
|
if (!provider) {
|
|
150
318
|
return null;
|
|
151
319
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
writeFileSync(this.authPath, "{}", "utf-8");
|
|
159
|
-
chmodSync(this.authPath, 0o600);
|
|
160
|
-
}
|
|
161
|
-
let release;
|
|
162
|
-
let lockCompromised = false;
|
|
163
|
-
let lockCompromisedError;
|
|
164
|
-
const throwIfLockCompromised = () => {
|
|
165
|
-
if (lockCompromised) {
|
|
166
|
-
throw lockCompromisedError ?? new Error("OAuth refresh lock was compromised");
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
try {
|
|
170
|
-
// Acquire exclusive lock with retry and timeout
|
|
171
|
-
// Use generous retry window to handle slow token endpoints
|
|
172
|
-
release = await lockfile.lock(this.authPath, {
|
|
173
|
-
retries: {
|
|
174
|
-
retries: 10,
|
|
175
|
-
factor: 2,
|
|
176
|
-
minTimeout: 100,
|
|
177
|
-
maxTimeout: 10000,
|
|
178
|
-
randomize: true,
|
|
179
|
-
},
|
|
180
|
-
stale: 30000, // Consider lock stale after 30 seconds
|
|
181
|
-
onCompromised: (err) => {
|
|
182
|
-
lockCompromised = true;
|
|
183
|
-
lockCompromisedError = err;
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
throwIfLockCompromised();
|
|
187
|
-
// Re-read file after acquiring lock - another instance may have refreshed
|
|
188
|
-
this.reload();
|
|
189
|
-
const cred = this.data[providerId];
|
|
320
|
+
const result = await this.storage.withLockAsync(async (current) => {
|
|
321
|
+
const currentData = this.parseStorageData(current);
|
|
322
|
+
this.data = currentData;
|
|
323
|
+
this.loadError = null;
|
|
324
|
+
const cred = currentData[providerId];
|
|
190
325
|
if (cred?.type !== "oauth") {
|
|
191
|
-
return null;
|
|
326
|
+
return { result: null };
|
|
192
327
|
}
|
|
193
|
-
// Check if token is still expired after re-reading
|
|
194
|
-
// (another instance may have already refreshed it)
|
|
195
328
|
if (Date.now() < cred.expires) {
|
|
196
|
-
|
|
197
|
-
throwIfLockCompromised();
|
|
198
|
-
const apiKey = provider.getApiKey(cred);
|
|
199
|
-
return { apiKey, newCredentials: cred };
|
|
329
|
+
return { result: { apiKey: provider.getApiKey(cred), newCredentials: cred } };
|
|
200
330
|
}
|
|
201
|
-
// Token still expired, we need to refresh
|
|
202
331
|
const oauthCreds = {};
|
|
203
|
-
for (const [key, value] of Object.entries(
|
|
332
|
+
for (const [key, value] of Object.entries(currentData)) {
|
|
204
333
|
if (value.type === "oauth") {
|
|
205
334
|
oauthCreds[key] = value;
|
|
206
335
|
}
|
|
207
336
|
}
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
this.data[providerId] = { type: "oauth", ...result.newCredentials };
|
|
212
|
-
this.save();
|
|
213
|
-
throwIfLockCompromised();
|
|
214
|
-
return result;
|
|
215
|
-
}
|
|
216
|
-
throwIfLockCompromised();
|
|
217
|
-
return null;
|
|
218
|
-
}
|
|
219
|
-
finally {
|
|
220
|
-
// Always release the lock
|
|
221
|
-
if (release) {
|
|
222
|
-
try {
|
|
223
|
-
await release();
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// Ignore unlock errors (lock may have been compromised)
|
|
227
|
-
}
|
|
337
|
+
const refreshed = await getOAuthApiKey(providerId, oauthCreds);
|
|
338
|
+
if (!refreshed) {
|
|
339
|
+
return { result: null };
|
|
228
340
|
}
|
|
229
|
-
|
|
341
|
+
const merged = {
|
|
342
|
+
...currentData,
|
|
343
|
+
[providerId]: { type: "oauth", ...refreshed.newCredentials },
|
|
344
|
+
};
|
|
345
|
+
this.data = merged;
|
|
346
|
+
this.loadError = null;
|
|
347
|
+
return { result: refreshed, next: JSON.stringify(merged, null, 2) };
|
|
348
|
+
});
|
|
349
|
+
return result;
|
|
230
350
|
}
|
|
231
351
|
/**
|
|
232
352
|
* Get API key for a provider.
|
|
@@ -237,7 +357,7 @@ export class AuthStorage {
|
|
|
237
357
|
* 4. Environment variable
|
|
238
358
|
* 5. Fallback resolver (models.json custom providers)
|
|
239
359
|
*/
|
|
240
|
-
async getApiKey(providerId) {
|
|
360
|
+
async getApiKey(providerId, options) {
|
|
241
361
|
// Runtime override takes highest priority
|
|
242
362
|
const runtimeKey = this.runtimeOverrides.get(providerId);
|
|
243
363
|
if (runtimeKey) {
|
|
@@ -263,7 +383,8 @@ export class AuthStorage {
|
|
|
263
383
|
return result.apiKey;
|
|
264
384
|
}
|
|
265
385
|
}
|
|
266
|
-
catch {
|
|
386
|
+
catch (error) {
|
|
387
|
+
this.recordError(error);
|
|
267
388
|
// Refresh failed - re-read file to check if another instance succeeded
|
|
268
389
|
this.reload();
|
|
269
390
|
const updatedCred = this.data[providerId];
|
|
@@ -286,7 +407,10 @@ export class AuthStorage {
|
|
|
286
407
|
if (envKey)
|
|
287
408
|
return envKey;
|
|
288
409
|
// Fall back to custom resolver (e.g., models.json custom providers)
|
|
289
|
-
|
|
410
|
+
if (options?.includeFallback !== false) {
|
|
411
|
+
return this.fallbackResolver?.(providerId) ?? undefined;
|
|
412
|
+
}
|
|
413
|
+
return undefined;
|
|
290
414
|
}
|
|
291
415
|
/**
|
|
292
416
|
* Get all registered OAuth providers
|