@deepseek-harness-tui/dsh-tui 0.9.0 → 0.9.2
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/README.md +69 -256
- package/bin/dsh-tui.js +172 -5
- package/cordis.patch.yml +16 -4
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
- package/dsh-ecosystem-spec/registry/README.md +14 -14
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
- package/lib/types/cc/hyperlink.d.ts +7 -1
- package/lib/types/cc/hyperlink.d.ts.map +1 -1
- package/lib/types/cc/hyperlink.js +3 -2
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +33 -3
- package/lib/types/cc/sessionColors.d.ts +27 -0
- package/lib/types/cc/sessionColors.d.ts.map +1 -0
- package/lib/types/cc/sessionColors.js +39 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +6 -0
- package/lib/types/components/AutoRecapRow.d.ts +23 -0
- package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
- package/lib/types/components/AutoRecapRow.js +23 -0
- package/lib/types/components/ColorPicker.d.ts +17 -0
- package/lib/types/components/ColorPicker.d.ts.map +1 -0
- package/lib/types/components/ColorPicker.js +20 -0
- package/lib/types/components/CommandSuggestions.d.ts +3 -1
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.d.ts +33 -0
- package/lib/types/components/ContextBarView.d.ts.map +1 -0
- package/lib/types/components/ContextBarView.js +46 -0
- package/lib/types/components/EffortInputBorder.d.ts +12 -1
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +22 -5
- package/lib/types/components/FileActionsPanel.d.ts +22 -0
- package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
- package/lib/types/components/FileActionsPanel.js +28 -0
- package/lib/types/components/FileSuggestions.d.ts +3 -1
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/MessageList.d.ts +3 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +13 -6
- package/lib/types/components/ModelPicker.d.ts +26 -4
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +26 -6
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +365 -57
- package/lib/types/components/RecapPanel.d.ts +25 -0
- package/lib/types/components/RecapPanel.d.ts.map +1 -0
- package/lib/types/components/RecapPanel.js +55 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +55 -4
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +6 -4
- package/lib/types/components/SuggestionCard.d.ts +4 -2
- package/lib/types/components/SuggestionCard.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.js +9 -2
- package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +2 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +36 -8
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +41 -9
- package/lib/types/dsh-adapter/channel.d.ts +101 -2
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +969 -93
- package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +8 -6
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +1 -0
- package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
- package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.d.ts +10 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +5 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +317 -8
- package/lib/types/dsh-adapter/presets.d.ts +18 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +31 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +89 -5
- package/lib/types/dsh-adapter/questions.d.ts +19 -2
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions.js +53 -10
- package/lib/types/dsh-adapter/recap.d.ts +49 -0
- package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
- package/lib/types/dsh-adapter/recap.js +121 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessionTree.js +954 -0
- package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
- package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
- package/lib/types/dsh-adapter/shortcuts.js +28 -165
- package/lib/types/dsh-adapter/types.d.ts +1 -1
- package/lib/types/dsh-adapter/types.d.ts.map +1 -1
- package/lib/types/herdr.js +1 -1
- package/lib/types/i18n.d.ts +604 -176
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +206 -72
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +21 -0
- package/lib/types/migrationPrefs.d.ts +60 -0
- package/lib/types/migrationPrefs.d.ts.map +1 -0
- package/lib/types/migrationPrefs.js +97 -0
- package/lib/types/modelGroups.d.ts +72 -0
- package/lib/types/modelGroups.d.ts.map +1 -0
- package/lib/types/modelGroups.js +97 -0
- package/lib/types/modelRecents.d.ts +39 -0
- package/lib/types/modelRecents.d.ts.map +1 -0
- package/lib/types/modelRecents.js +97 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
- package/lib/types/native-ts/yoga-layout/index.js +31 -1
- package/lib/types/oauth.d.ts +15 -0
- package/lib/types/oauth.d.ts.map +1 -0
- package/lib/types/oauth.js +14 -0
- package/lib/types/plugin-spec/registry.d.ts +1 -1
- package/lib/types/plugin-spec/registry.js +1 -1
- package/lib/types/reload.d.ts +109 -0
- package/lib/types/reload.d.ts.map +1 -0
- package/lib/types/reload.js +118 -0
- package/lib/types/screens/Chat.d.ts +3 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +588 -64
- package/lib/types/screens/SessionTree.d.ts +27 -0
- package/lib/types/screens/SessionTree.d.ts.map +1 -0
- package/lib/types/screens/SessionTree.js +555 -0
- package/lib/types/screens/StatusLine.d.ts +0 -9
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +215 -52
- package/lib/types/screens/StatusMetrics.d.ts +35 -3
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +63 -8
- package/lib/types/screens/chatOverlay.d.ts +17 -1
- package/lib/types/screens/chatOverlay.d.ts.map +1 -1
- package/lib/types/screens/chatOverlay.js +3 -1
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +22 -4
- package/lib/types/update.d.ts +159 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +496 -30
- package/lib/types/utils/debug.d.ts +1 -1
- package/lib/types/utils/debug.js +3 -3
- package/lib/types/utils/fileTarget.d.ts +46 -0
- package/lib/types/utils/fileTarget.d.ts.map +1 -0
- package/lib/types/utils/fileTarget.js +178 -0
- package/lib/types/utils/keymap.d.ts +134 -0
- package/lib/types/utils/keymap.d.ts.map +1 -0
- package/lib/types/utils/keymap.js +311 -0
- package/lib/types/utils/mentions.d.ts +25 -2
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +69 -10
- package/lib/types/utils/openExternal.d.ts +50 -0
- package/lib/types/utils/openExternal.d.ts.map +1 -0
- package/lib/types/utils/openExternal.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/command/LICENSE +21 -21
- package/node_modules/@dsh-std/command/README.md +26 -26
- package/node_modules/@dsh-std/command/README.zh.md +28 -28
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/command/package.json +22 -22
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/node_modules/@dsh-std/connection/README.md +42 -42
- package/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/node_modules/@dsh-std/connection/package.json +20 -20
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/core/LICENSE +21 -21
- package/node_modules/@dsh-std/core/README.md +13 -13
- package/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/core/package.json +21 -21
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/node_modules/@dsh-std/manifest/README.md +5 -5
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/manifest/package.json +20 -20
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/messages/LICENSE +21 -21
- package/node_modules/@dsh-std/messages/README.md +5 -5
- package/node_modules/@dsh-std/messages/README.zh.md +7 -7
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/messages/package.json +20 -20
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/node_modules/@dsh-std/presentation/README.md +27 -27
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/presentation/package.json +25 -25
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/storage/LICENSE +21 -21
- package/node_modules/@dsh-std/storage/README.md +5 -5
- package/node_modules/@dsh-std/storage/README.zh.md +7 -7
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/storage/package.json +20 -20
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
- package/package.json +17 -5
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/instruction-hint.mjs +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/auth` command: a textual veneer over the {@link DshAuthApi} — status
|
|
3
|
+
* listing, login (optionally picking the provider interactively), logout.
|
|
4
|
+
*
|
|
5
|
+
* @module dsh-auth/command
|
|
6
|
+
*/
|
|
7
|
+
const USAGE = 'Usage: /auth [status] | /auth login [provider] | /auth logout <provider>';
|
|
8
|
+
function renderStatus(rows) {
|
|
9
|
+
const lines = rows.map(row => {
|
|
10
|
+
const state = row.signedIn
|
|
11
|
+
? `signed in — token expires ${new Date(row.expiresAt ?? 0).toISOString()}`
|
|
12
|
+
: row.expired
|
|
13
|
+
? 'signed in, token expired — /auth login to refresh'
|
|
14
|
+
: 'not signed in';
|
|
15
|
+
return ` ${row.provider.padEnd(14)} ${state}`;
|
|
16
|
+
});
|
|
17
|
+
return ['dsh-auth providers:', ...lines].join('\n');
|
|
18
|
+
}
|
|
19
|
+
/** Split the raw input into at most two lowercase words: verb and target. */
|
|
20
|
+
function parseArgs(raw) {
|
|
21
|
+
const words = raw.trim().split(/\s+/u).filter(word => word !== '');
|
|
22
|
+
const verb = words[0]?.toLowerCase();
|
|
23
|
+
const target = words[1];
|
|
24
|
+
return { verb: words.length === 0 ? undefined : verb, target };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build the `/auth` handler. Every failure returns a `CommandResult`, never a
|
|
28
|
+
* rejection the host would have to translate; unexpected throws still
|
|
29
|
+
* propagate for the host's own error surface.
|
|
30
|
+
*/
|
|
31
|
+
export function createAuthCommandHandler(api) {
|
|
32
|
+
return async (invocation) => {
|
|
33
|
+
const { verb, target } = parseArgs(invocation.rawInput);
|
|
34
|
+
if (verb === undefined || verb === 'status') {
|
|
35
|
+
if (target !== undefined)
|
|
36
|
+
return { kind: 'error', text: USAGE };
|
|
37
|
+
return { kind: 'success', text: renderStatus(await api.providers()) };
|
|
38
|
+
}
|
|
39
|
+
if (verb === 'login') {
|
|
40
|
+
try {
|
|
41
|
+
const result = await api.login(target, invocation.signal);
|
|
42
|
+
return {
|
|
43
|
+
kind: 'success',
|
|
44
|
+
text: `Signed in to ${result.oauthLabel} (${result.provider}); token expires `
|
|
45
|
+
+ `${new Date(result.expiresAt).toISOString()}. Its models are selectable via /model.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { kind: 'error', text: `dsh-auth: ${error instanceof Error ? error.message : String(error)}` };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (verb === 'logout') {
|
|
53
|
+
if (target === undefined)
|
|
54
|
+
return { kind: 'error', text: USAGE };
|
|
55
|
+
try {
|
|
56
|
+
const removed = await api.logout(target);
|
|
57
|
+
return removed
|
|
58
|
+
? { kind: 'success', text: `Signed out of ${target}; its stored credential was removed.` }
|
|
59
|
+
: { kind: 'error', text: `${target} was not signed in.` };
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
return { kind: 'error', text: `dsh-auth: ${error instanceof Error ? error.message : String(error)}` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { kind: 'error', text: USAGE };
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed OAuth credential persistence: a pi-ai `CredentialStore` over
|
|
3
|
+
* one JSON document, one credential per provider id.
|
|
4
|
+
*
|
|
5
|
+
* Writes are atomic (temp file + rename) with 0700 directory / 0600 file
|
|
6
|
+
* permissions best-effort on every platform. All mutations go through
|
|
7
|
+
* {@link CredentialFile.modify}, which serializes read-modify-write cycles
|
|
8
|
+
* per provider in-process — pi-ai runs its OAuth refresh *inside* `modify`,
|
|
9
|
+
* so the exclusion here is what keeps concurrent requests from
|
|
10
|
+
* double-refreshing a rotated token. The file is the single source of truth;
|
|
11
|
+
* nothing here ever logs token material, and {@link CredentialFile.describe}
|
|
12
|
+
* reports only non-secret metadata for status surfaces.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-auth/credentials
|
|
15
|
+
*/
|
|
16
|
+
import type { Credential, CredentialInfo, CredentialStore } from '@earendil-works/pi-ai';
|
|
17
|
+
/** The stored credential shape: a pi-ai `OAuthCredential`. */
|
|
18
|
+
export interface StoredOAuthCredential {
|
|
19
|
+
type: 'oauth';
|
|
20
|
+
access: string;
|
|
21
|
+
refresh: string;
|
|
22
|
+
expires: number;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
/** Narrow an unknown parsed value into a stored credential, or reject it. */
|
|
26
|
+
export declare function asStoredCredential(value: unknown): StoredOAuthCredential | undefined;
|
|
27
|
+
/** Default credential file location: `$DSH_HOME/dsh-auth/credentials.json` (or `~/.dsh/…`). */
|
|
28
|
+
export declare function defaultCredentialsFile(): string;
|
|
29
|
+
/**
|
|
30
|
+
* The credential file. IO failures throw (loud, naming the path) rather than
|
|
31
|
+
* degrading to an empty store: silently treating a corrupt or unreadable
|
|
32
|
+
* credential file as "signed out everywhere" would strand every route behind
|
|
33
|
+
* a fresh login for no reason.
|
|
34
|
+
*/
|
|
35
|
+
export declare class CredentialFile implements CredentialStore {
|
|
36
|
+
readonly path: string;
|
|
37
|
+
/** Per-provider operation chains: modify/delete never overlap for one id. */
|
|
38
|
+
private readonly chains;
|
|
39
|
+
private cache;
|
|
40
|
+
constructor(path: string);
|
|
41
|
+
/** The stored credential for one provider, possibly expired. */
|
|
42
|
+
read(providerId: string): Promise<Credential | undefined>;
|
|
43
|
+
/** Stored credential metadata without resolving or exposing secrets. */
|
|
44
|
+
list(): Promise<readonly CredentialInfo[]>;
|
|
45
|
+
/**
|
|
46
|
+
* Serialized read-modify-write for one provider. `fn` sees the current
|
|
47
|
+
* credential; returning a new credential persists it, returning
|
|
48
|
+
* `undefined` leaves the entry unchanged. Resolves with the post-write
|
|
49
|
+
* credential. Rejections from `fn` propagate without touching the file.
|
|
50
|
+
*/
|
|
51
|
+
modify(providerId: string, fn: (current: Credential | undefined) => Promise<Credential | undefined>): Promise<Credential | undefined>;
|
|
52
|
+
/** Remove one provider's credential (logout). */
|
|
53
|
+
delete(providerId: string): Promise<void>;
|
|
54
|
+
/** Non-secret metadata for every stored credential, for status surfaces. */
|
|
55
|
+
describe(): Promise<readonly {
|
|
56
|
+
provider: string;
|
|
57
|
+
expiresAt: number;
|
|
58
|
+
expired: boolean;
|
|
59
|
+
}[]>;
|
|
60
|
+
/** Run one operation after every earlier operation for the same provider. */
|
|
61
|
+
private chain;
|
|
62
|
+
private load;
|
|
63
|
+
private save;
|
|
64
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed OAuth credential persistence: a pi-ai `CredentialStore` over
|
|
3
|
+
* one JSON document, one credential per provider id.
|
|
4
|
+
*
|
|
5
|
+
* Writes are atomic (temp file + rename) with 0700 directory / 0600 file
|
|
6
|
+
* permissions best-effort on every platform. All mutations go through
|
|
7
|
+
* {@link CredentialFile.modify}, which serializes read-modify-write cycles
|
|
8
|
+
* per provider in-process — pi-ai runs its OAuth refresh *inside* `modify`,
|
|
9
|
+
* so the exclusion here is what keeps concurrent requests from
|
|
10
|
+
* double-refreshing a rotated token. The file is the single source of truth;
|
|
11
|
+
* nothing here ever logs token material, and {@link CredentialFile.describe}
|
|
12
|
+
* reports only non-secret metadata for status surfaces.
|
|
13
|
+
*
|
|
14
|
+
* @module dsh-auth/credentials
|
|
15
|
+
*/
|
|
16
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
17
|
+
import { dirname, join } from 'node:path';
|
|
18
|
+
import { homedir } from 'node:os';
|
|
19
|
+
/** Narrow an unknown parsed value into a stored credential, or reject it. */
|
|
20
|
+
export function asStoredCredential(value) {
|
|
21
|
+
if (typeof value !== 'object' || value === null)
|
|
22
|
+
return undefined;
|
|
23
|
+
const record = value;
|
|
24
|
+
if (record['type'] !== 'oauth')
|
|
25
|
+
return undefined;
|
|
26
|
+
if (typeof record['access'] !== 'string' || typeof record['refresh'] !== 'string')
|
|
27
|
+
return undefined;
|
|
28
|
+
if (typeof record['expires'] !== 'number' || !Number.isFinite(record['expires']))
|
|
29
|
+
return undefined;
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
/** Default credential file location: `$DSH_HOME/dsh-auth/credentials.json` (or `~/.dsh/…`). */
|
|
33
|
+
export function defaultCredentialsFile() {
|
|
34
|
+
const override = process.env['DSH_AUTH_CREDENTIALS'];
|
|
35
|
+
if (override !== undefined && override !== '')
|
|
36
|
+
return override;
|
|
37
|
+
const root = process.env['DSH_HOME'] ?? join(homedir(), '.dsh');
|
|
38
|
+
return join(root, 'dsh-auth', 'credentials.json');
|
|
39
|
+
}
|
|
40
|
+
const EMPTY_DOCUMENT = { version: 1, providers: {} };
|
|
41
|
+
/**
|
|
42
|
+
* The credential file. IO failures throw (loud, naming the path) rather than
|
|
43
|
+
* degrading to an empty store: silently treating a corrupt or unreadable
|
|
44
|
+
* credential file as "signed out everywhere" would strand every route behind
|
|
45
|
+
* a fresh login for no reason.
|
|
46
|
+
*/
|
|
47
|
+
export class CredentialFile {
|
|
48
|
+
path;
|
|
49
|
+
/** Per-provider operation chains: modify/delete never overlap for one id. */
|
|
50
|
+
chains = new Map();
|
|
51
|
+
cache;
|
|
52
|
+
constructor(path) {
|
|
53
|
+
this.path = path;
|
|
54
|
+
}
|
|
55
|
+
/** The stored credential for one provider, possibly expired. */
|
|
56
|
+
async read(providerId) {
|
|
57
|
+
return (await this.load()).providers[providerId];
|
|
58
|
+
}
|
|
59
|
+
/** Stored credential metadata without resolving or exposing secrets. */
|
|
60
|
+
async list() {
|
|
61
|
+
const document = await this.load();
|
|
62
|
+
return Object.entries(document.providers).map(([providerId, credential]) => ({
|
|
63
|
+
providerId,
|
|
64
|
+
type: credential.type,
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Serialized read-modify-write for one provider. `fn` sees the current
|
|
69
|
+
* credential; returning a new credential persists it, returning
|
|
70
|
+
* `undefined` leaves the entry unchanged. Resolves with the post-write
|
|
71
|
+
* credential. Rejections from `fn` propagate without touching the file.
|
|
72
|
+
*/
|
|
73
|
+
async modify(providerId, fn) {
|
|
74
|
+
return this.chain(providerId, async () => {
|
|
75
|
+
const document = await this.load();
|
|
76
|
+
const current = document.providers[providerId];
|
|
77
|
+
const replacement = await fn(current);
|
|
78
|
+
if (replacement === undefined || replacement === current)
|
|
79
|
+
return current;
|
|
80
|
+
if (replacement.type !== 'oauth') {
|
|
81
|
+
throw new Error(`dsh-auth: refusing to store a "${replacement.type}" credential for "${providerId}" — this store holds OAuth credentials only`);
|
|
82
|
+
}
|
|
83
|
+
await this.save({ ...document, providers: { ...document.providers, [providerId]: replacement } });
|
|
84
|
+
return replacement;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Remove one provider's credential (logout). */
|
|
88
|
+
async delete(providerId) {
|
|
89
|
+
await this.chain(providerId, async () => {
|
|
90
|
+
const document = await this.load();
|
|
91
|
+
if (!(providerId in document.providers))
|
|
92
|
+
return;
|
|
93
|
+
const providers = { ...document.providers };
|
|
94
|
+
delete providers[providerId];
|
|
95
|
+
await this.save({ ...document, providers });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Non-secret metadata for every stored credential, for status surfaces. */
|
|
99
|
+
async describe() {
|
|
100
|
+
const document = await this.load();
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
return Object.entries(document.providers)
|
|
103
|
+
.filter((entry) => entry[1].type === 'oauth')
|
|
104
|
+
.map(([provider, credential]) => ({
|
|
105
|
+
provider,
|
|
106
|
+
expiresAt: credential.expires,
|
|
107
|
+
expired: credential.expires <= now,
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
/** Run one operation after every earlier operation for the same provider. */
|
|
111
|
+
chain(provider, operation) {
|
|
112
|
+
const run = (this.chains.get(provider) ?? Promise.resolve()).then(operation, operation);
|
|
113
|
+
this.chains.set(provider, run.then(() => undefined, () => undefined));
|
|
114
|
+
return run;
|
|
115
|
+
}
|
|
116
|
+
async load() {
|
|
117
|
+
if (this.cache !== undefined)
|
|
118
|
+
return this.cache;
|
|
119
|
+
let text;
|
|
120
|
+
try {
|
|
121
|
+
text = readFileSync(this.path, 'utf8');
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error?.code === 'ENOENT') {
|
|
125
|
+
this.cache = EMPTY_DOCUMENT;
|
|
126
|
+
return this.cache;
|
|
127
|
+
}
|
|
128
|
+
throw new Error(`dsh-auth: cannot read credential file ${this.path}: ${String(error)}`);
|
|
129
|
+
}
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(text);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
throw new Error(`dsh-auth: credential file ${this.path} is not valid JSON (${String(error)}); `
|
|
136
|
+
+ 'fix or remove the file by hand — it will not be overwritten silently');
|
|
137
|
+
}
|
|
138
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
139
|
+
throw new Error(`dsh-auth: credential file ${this.path} has an unexpected shape; fix or remove it by hand`);
|
|
140
|
+
}
|
|
141
|
+
const record = parsed;
|
|
142
|
+
if (record['version'] !== 1 || typeof record['providers'] !== 'object' || record['providers'] === null) {
|
|
143
|
+
throw new Error(`dsh-auth: credential file ${this.path} has an unexpected shape; fix or remove it by hand`);
|
|
144
|
+
}
|
|
145
|
+
const providers = {};
|
|
146
|
+
for (const [provider, value] of Object.entries(record['providers'])) {
|
|
147
|
+
const credential = asStoredCredential(value);
|
|
148
|
+
if (credential === undefined) {
|
|
149
|
+
throw new Error(`dsh-auth: credential file ${this.path} holds an invalid entry for "${provider}"; fix or remove it by hand`);
|
|
150
|
+
}
|
|
151
|
+
providers[provider] = credential;
|
|
152
|
+
}
|
|
153
|
+
this.cache = { version: 1, providers };
|
|
154
|
+
return this.cache;
|
|
155
|
+
}
|
|
156
|
+
async save(document) {
|
|
157
|
+
const text = JSON.stringify(document, null, 2) + '\n';
|
|
158
|
+
const directory = dirname(this.path);
|
|
159
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
160
|
+
const temporary = join(directory, `.${Math.random().toString(36).slice(2)}.tmp`);
|
|
161
|
+
try {
|
|
162
|
+
writeFileSync(temporary, text, { mode: 0o600 });
|
|
163
|
+
renameSync(temporary, this.path);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
throw new Error(`dsh-auth: cannot write credential file ${this.path}: ${String(error)}`);
|
|
167
|
+
}
|
|
168
|
+
this.cache = document;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-auth — subscription OAuth sign-in as LLM provider routes.
|
|
3
|
+
*
|
|
4
|
+
* One cordis plugin mounts the pi-ai catalog providers that ship OAuth flows
|
|
5
|
+
* (ChatGPT/Codex, Claude Pro/Max, SuperGrok) as `llm` registry routes, so
|
|
6
|
+
* their models appear in every model picker the moment the plugin loads —
|
|
7
|
+
* signing in is the only missing credential. `PiAiAdapter` runs with this
|
|
8
|
+
* plugin's file-backed pi-ai `CredentialStore` injected
|
|
9
|
+
* (`PiAiAuthInjection`): requests resolve the stored OAuth credential through
|
|
10
|
+
* the provider's own auth and rotate refresh tokens under the store's lock.
|
|
11
|
+
* Login/logout run over the `userQuestions` seam, so they work on any
|
|
12
|
+
* interactive surface and refuse cleanly where none exists (TUI-RUN-001).
|
|
13
|
+
*
|
|
14
|
+
* ```yaml
|
|
15
|
+
* - id: dsh-auth
|
|
16
|
+
* name: 'dsh-auth'
|
|
17
|
+
* config:
|
|
18
|
+
* providers: [openai-codex, anthropic, xai] # subset of the mounted set
|
|
19
|
+
* # credentialsFile: /secure/path/credentials.json # default $DSH_HOME/dsh-auth/
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Routes register individually: a route another adapter family already owns
|
|
23
|
+
* (an llm-pi-ai settings profile for the same provider) is refused by the
|
|
24
|
+
* registry — that refusal is logged and the remaining routes still mount.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-auth
|
|
27
|
+
*/
|
|
28
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
29
|
+
import z from '@deepseek-ai/schemastery';
|
|
30
|
+
import type { LlmModelInfo } from '@deepseek-ai/dsh-llm';
|
|
31
|
+
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai';
|
|
32
|
+
import type { PiAiAdapterOptions } from '@deepseek-ai/dsh-llm-pi-ai';
|
|
33
|
+
export declare const name = "dsh-auth";
|
|
34
|
+
/**
|
|
35
|
+
* Deliberately empty. A hard code-level inject would deadlock any
|
|
36
|
+
* composition lacking the `llm`/`commands` services at boot ("pending
|
|
37
|
+
* (waiting for service: …)") — the failure mode dsh-tui documented for its
|
|
38
|
+
* own optional rows (#183). Both services resolve per call through
|
|
39
|
+
* `ctx.get` in {@link apply}; compositions that guarantee them (the
|
|
40
|
+
* dsh-tui patch row, this package's bundle patch) declare the inject at the
|
|
41
|
+
* *entry* level, where a stale patch cannot deadlock a newer boot.
|
|
42
|
+
*/
|
|
43
|
+
export declare const inject: readonly string[];
|
|
44
|
+
/** Plugin configuration. */
|
|
45
|
+
export interface Config {
|
|
46
|
+
/** Provider routes to mount; every entry must ship an OAuth flow in the installed pi-ai catalog. */
|
|
47
|
+
providers?: string[];
|
|
48
|
+
/** Credential file override; default `$DSH_HOME/dsh-auth/credentials.json`. */
|
|
49
|
+
credentialsFile?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare const Config: z<Config>;
|
|
52
|
+
export type { DshAuthApi, DshAuthLoginResult, DshAuthSignInStatus, DshAuthService } from './service.js';
|
|
53
|
+
export { createDshAuthApi } from './service.js';
|
|
54
|
+
export { QuestionBridge, describeEvent } from './interaction.js';
|
|
55
|
+
export type { AskFn, QuestionBridgeHelpers } from './interaction.js';
|
|
56
|
+
export { copyToClipboard, openInBrowser, openerFor } from './opener.js';
|
|
57
|
+
export { CredentialFile, defaultCredentialsFile } from './credentials.js';
|
|
58
|
+
export { OAUTH_PROVIDER_IDS, buildOAuthProfile } from './profiles.js';
|
|
59
|
+
/**
|
|
60
|
+
* The adapter the plugin registers: a {@link PiAiAdapter} whose *advisory
|
|
61
|
+
* catalog* is credential-gated. A provider with no stored OAuth credential
|
|
62
|
+
* lists no models — its rows never reach any model picker, which is the
|
|
63
|
+
* whole point: picking a model that would only fail with "not signed in"
|
|
64
|
+
* is noise. The gate only shapes `listModels`; `resolveModel` and requests
|
|
65
|
+
* are untouched, so a model id already saved in a session (or named
|
|
66
|
+
* explicitly) keeps resolving exactly as the registry contract promises
|
|
67
|
+
* ("advisory and never changes routing").
|
|
68
|
+
*
|
|
69
|
+
* An expired-but-stored credential still lists: token refresh runs on the
|
|
70
|
+
* next request, and a picker that hid a refreshable provider would look
|
|
71
|
+
* signed-out when it is not.
|
|
72
|
+
*/
|
|
73
|
+
export declare class CredentialGatedAdapter extends PiAiAdapter {
|
|
74
|
+
private readonly hasCredential;
|
|
75
|
+
constructor(options: PiAiAdapterOptions, hasCredential: (provider: string) => Promise<boolean>);
|
|
76
|
+
listModels(provider: string): Promise<readonly LlmModelInfo[]>;
|
|
77
|
+
}
|
|
78
|
+
/** Mount the routes, the service, and the command. */
|
|
79
|
+
export declare function apply(ctx: Context, config: Config): void;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-auth — subscription OAuth sign-in as LLM provider routes.
|
|
3
|
+
*
|
|
4
|
+
* One cordis plugin mounts the pi-ai catalog providers that ship OAuth flows
|
|
5
|
+
* (ChatGPT/Codex, Claude Pro/Max, SuperGrok) as `llm` registry routes, so
|
|
6
|
+
* their models appear in every model picker the moment the plugin loads —
|
|
7
|
+
* signing in is the only missing credential. `PiAiAdapter` runs with this
|
|
8
|
+
* plugin's file-backed pi-ai `CredentialStore` injected
|
|
9
|
+
* (`PiAiAuthInjection`): requests resolve the stored OAuth credential through
|
|
10
|
+
* the provider's own auth and rotate refresh tokens under the store's lock.
|
|
11
|
+
* Login/logout run over the `userQuestions` seam, so they work on any
|
|
12
|
+
* interactive surface and refuse cleanly where none exists (TUI-RUN-001).
|
|
13
|
+
*
|
|
14
|
+
* ```yaml
|
|
15
|
+
* - id: dsh-auth
|
|
16
|
+
* name: 'dsh-auth'
|
|
17
|
+
* config:
|
|
18
|
+
* providers: [openai-codex, anthropic, xai] # subset of the mounted set
|
|
19
|
+
* # credentialsFile: /secure/path/credentials.json # default $DSH_HOME/dsh-auth/
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Routes register individually: a route another adapter family already owns
|
|
23
|
+
* (an llm-pi-ai settings profile for the same provider) is refused by the
|
|
24
|
+
* registry — that refusal is logged and the remaining routes still mount.
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-auth
|
|
27
|
+
*/
|
|
28
|
+
import { existsSync } from 'node:fs';
|
|
29
|
+
import { homedir } from 'node:os';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import z from '@deepseek-ai/schemastery';
|
|
32
|
+
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai';
|
|
33
|
+
import { CredentialFile, defaultCredentialsFile } from './credentials.js';
|
|
34
|
+
import { buildOAuthProfile, OAUTH_PROVIDER_IDS } from './profiles.js';
|
|
35
|
+
import { createDshAuthApi, DshAuthService } from './service.js';
|
|
36
|
+
import { createAuthCommandHandler } from './command.js';
|
|
37
|
+
export const name = 'dsh-auth';
|
|
38
|
+
/**
|
|
39
|
+
* Deliberately empty. A hard code-level inject would deadlock any
|
|
40
|
+
* composition lacking the `llm`/`commands` services at boot ("pending
|
|
41
|
+
* (waiting for service: …)") — the failure mode dsh-tui documented for its
|
|
42
|
+
* own optional rows (#183). Both services resolve per call through
|
|
43
|
+
* `ctx.get` in {@link apply}; compositions that guarantee them (the
|
|
44
|
+
* dsh-tui patch row, this package's bundle patch) declare the inject at the
|
|
45
|
+
* *entry* level, where a stale patch cannot deadlock a newer boot.
|
|
46
|
+
*/
|
|
47
|
+
export const inject = [];
|
|
48
|
+
export const Config = z.object({
|
|
49
|
+
providers: z.array(z.string()).default([...OAUTH_PROVIDER_IDS]),
|
|
50
|
+
credentialsFile: z.string(),
|
|
51
|
+
});
|
|
52
|
+
export { createDshAuthApi } from './service.js';
|
|
53
|
+
export { QuestionBridge, describeEvent } from './interaction.js';
|
|
54
|
+
export { copyToClipboard, openInBrowser, openerFor } from './opener.js';
|
|
55
|
+
export { CredentialFile, defaultCredentialsFile } from './credentials.js';
|
|
56
|
+
export { OAUTH_PROVIDER_IDS, buildOAuthProfile } from './profiles.js';
|
|
57
|
+
/**
|
|
58
|
+
* The ambient auth context providers may consult while resolving their own
|
|
59
|
+
* auth. `env()` answers from the process environment; `fileExists()` answers
|
|
60
|
+
* about the host process's filesystem (the paths a provider asks about —
|
|
61
|
+
* `~/.aws/credentials` and friends — are facts about where this process
|
|
62
|
+
* runs, not about the project under edit).
|
|
63
|
+
*/
|
|
64
|
+
function hostAuthContext() {
|
|
65
|
+
return {
|
|
66
|
+
env: async (name) => process.env[name],
|
|
67
|
+
fileExists: path => Promise.resolve(path.startsWith('~/') ? existsSync(join(homedir(), path.slice(2))) : existsSync(path)),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The adapter the plugin registers: a {@link PiAiAdapter} whose *advisory
|
|
72
|
+
* catalog* is credential-gated. A provider with no stored OAuth credential
|
|
73
|
+
* lists no models — its rows never reach any model picker, which is the
|
|
74
|
+
* whole point: picking a model that would only fail with "not signed in"
|
|
75
|
+
* is noise. The gate only shapes `listModels`; `resolveModel` and requests
|
|
76
|
+
* are untouched, so a model id already saved in a session (or named
|
|
77
|
+
* explicitly) keeps resolving exactly as the registry contract promises
|
|
78
|
+
* ("advisory and never changes routing").
|
|
79
|
+
*
|
|
80
|
+
* An expired-but-stored credential still lists: token refresh runs on the
|
|
81
|
+
* next request, and a picker that hid a refreshable provider would look
|
|
82
|
+
* signed-out when it is not.
|
|
83
|
+
*/
|
|
84
|
+
export class CredentialGatedAdapter extends PiAiAdapter {
|
|
85
|
+
hasCredential;
|
|
86
|
+
constructor(options, hasCredential) {
|
|
87
|
+
super(options);
|
|
88
|
+
this.hasCredential = hasCredential;
|
|
89
|
+
}
|
|
90
|
+
async listModels(provider) {
|
|
91
|
+
if (!(await this.hasCredential(provider)))
|
|
92
|
+
return [];
|
|
93
|
+
return super.listModels(provider);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** Mount the routes, the service, and the command. */
|
|
97
|
+
export function apply(ctx, config) {
|
|
98
|
+
const configured = config.providers ?? [...OAUTH_PROVIDER_IDS];
|
|
99
|
+
const unknown = configured.filter(id => !OAUTH_PROVIDER_IDS.includes(id));
|
|
100
|
+
if (unknown.length > 0 || configured.length === 0) {
|
|
101
|
+
throw new Error(`dsh-auth: providers must be a non-empty subset of [${OAUTH_PROVIDER_IDS.join(', ')}]; got [${configured.join(', ')}]`);
|
|
102
|
+
}
|
|
103
|
+
// Profile construction validates the installed catalog loudly: a pi-ai
|
|
104
|
+
// downgrade that dropped a provider fails the boot that asked for it.
|
|
105
|
+
const profiles = new Map(configured.map(id => [id, buildOAuthProfile(id)]));
|
|
106
|
+
const store = new CredentialFile(config.credentialsFile ?? defaultCredentialsFile());
|
|
107
|
+
// Fail closed on store trouble: a credential file that cannot be read
|
|
108
|
+
// must not surface forty models that would all fail at request time.
|
|
109
|
+
const hasCredential = async (provider) => {
|
|
110
|
+
try {
|
|
111
|
+
return await store.read(provider) !== undefined;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const adapter = new CredentialGatedAdapter({
|
|
118
|
+
// One immutable map for the plugin's lifetime: the snapshot memoizes on
|
|
119
|
+
// identity, and route changes here always mean a plugin remount anyway.
|
|
120
|
+
profiles: () => profiles,
|
|
121
|
+
// No route ever names an api-key credential, so the override stays
|
|
122
|
+
// undefined and every request authenticates through the collection's
|
|
123
|
+
// own auth — the OAuth credential this plugin's store holds.
|
|
124
|
+
resolveApiKey: async () => undefined,
|
|
125
|
+
auth: {
|
|
126
|
+
credentials: store,
|
|
127
|
+
authContext: hostAuthContext(),
|
|
128
|
+
},
|
|
129
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
130
|
+
onReplayDegrade: ({ provider, model, reason }) => {
|
|
131
|
+
ctx.logger.warn(`dsh-auth: replay state on assistant history for ${provider}/${model} degraded: ${reason}`);
|
|
132
|
+
},
|
|
133
|
+
}, hasCredential);
|
|
134
|
+
// The service is a thin holder so UIs can find the api without importing
|
|
135
|
+
// the plugin module; the defensive get-then-create matches how the TUI
|
|
136
|
+
// mounts userQuestions.
|
|
137
|
+
const service = ctx.get('dshAuth') ?? new DshAuthService(ctx);
|
|
138
|
+
const api = createDshAuthApi({
|
|
139
|
+
profiles,
|
|
140
|
+
store,
|
|
141
|
+
resolveAsk: () => {
|
|
142
|
+
const questions = ctx.get('userQuestions');
|
|
143
|
+
return questions === undefined ? undefined : request => questions.ask(request);
|
|
144
|
+
},
|
|
145
|
+
logger: ctx.logger,
|
|
146
|
+
});
|
|
147
|
+
service.api = api;
|
|
148
|
+
ctx.effect(function* () {
|
|
149
|
+
const releases = [];
|
|
150
|
+
// Runtime service resolution (never a code-level inject — see `inject`):
|
|
151
|
+
// absent services keep this plugin inert and logged, not the whole boot
|
|
152
|
+
// tree deadlocked.
|
|
153
|
+
const llm = ctx.get('llm');
|
|
154
|
+
const commands = ctx.get('commands');
|
|
155
|
+
if (llm === undefined) {
|
|
156
|
+
ctx.logger.warn('dsh-auth: no llm service mounted — provider routes stay unregistered');
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
// Individual registrations: one conflicting route must not strand the
|
|
160
|
+
// rest (the registry keeps the previous owner serving).
|
|
161
|
+
for (const id of configured) {
|
|
162
|
+
try {
|
|
163
|
+
releases.push(llm.registerAdapter([id], adapter));
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
ctx.logger.error(`dsh-auth: route "${id}" was not registered: ${error instanceof Error ? error.message : String(error)} `
|
|
167
|
+
+ '(another adapter family may own it — check the llm-pi-ai settings section)');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const active = new Set();
|
|
172
|
+
if (commands === undefined) {
|
|
173
|
+
ctx.logger.warn('dsh-auth: no commands service mounted — the /auth command stays unregistered');
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const handler = createAuthCommandHandler(api);
|
|
177
|
+
releases.push(commands.register({
|
|
178
|
+
name: 'auth',
|
|
179
|
+
description: 'Provider subscription sign-in (OAuth): status, login, logout',
|
|
180
|
+
handler: invocation => {
|
|
181
|
+
const operation = handler(invocation);
|
|
182
|
+
active.add(operation);
|
|
183
|
+
void operation.then(() => active.delete(operation), () => active.delete(operation));
|
|
184
|
+
return operation;
|
|
185
|
+
},
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
// Drain before releasing: LIFO composite teardown lets no new invocation
|
|
189
|
+
// enter while already-started logins finish their final write.
|
|
190
|
+
yield async () => { await Promise.allSettled([...active]); };
|
|
191
|
+
yield () => {
|
|
192
|
+
for (const release of releases)
|
|
193
|
+
release();
|
|
194
|
+
};
|
|
195
|
+
}, 'dsh-auth lifecycle');
|
|
196
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `AuthInteraction` bridge: pi-ai OAuth flows speak prompts and events;
|
|
3
|
+
* every interactive surface in this ecosystem speaks `ctx.userQuestions`.
|
|
4
|
+
*
|
|
5
|
+
* `prompt()` maps one pi-ai prompt onto one question — `select` prompts carry
|
|
6
|
+
* their options, text/secret/manual-code prompts expect the custom-answer
|
|
7
|
+
* input row.
|
|
8
|
+
*
|
|
9
|
+
* `notify()` restores the host contract pi-ai's browser flows assume: the
|
|
10
|
+
* `auth_url` / `device_code` events expect the *host* to open the browser
|
|
11
|
+
* (their authorize URLs run hundreds of characters — wrapped across panel
|
|
12
|
+
* lines they are unclickable, and hand-copying picks up wrap artifacts that
|
|
13
|
+
* corrupt the encoded `redirect_uri`, which the provider then rejects). So
|
|
14
|
+
* the bridge opens the URL itself, then shows one waiting panel carrying
|
|
15
|
+
* copy / reopen / cancel actions until the flow settles. The device-code
|
|
16
|
+
* path gets the same treatment: its short code is the thing to copy, and
|
|
17
|
+
* the verification page is auto-opened too.
|
|
18
|
+
*
|
|
19
|
+
* One `AbortController` owns the whole run: pi-ai reads `interaction.signal`
|
|
20
|
+
* to abort the flow, every ask request carries it so surfaces close their
|
|
21
|
+
* panels, and the waiting panel's cancel action fires it.
|
|
22
|
+
*
|
|
23
|
+
* Nothing here assumes a browser or GUI *exists* (TUI-RUN-001): when no
|
|
24
|
+
* opener or clipboard helper is available the panel degrades to showing the
|
|
25
|
+
* URL / code verbatim with a clear note, and where no question provider is
|
|
26
|
+
* registered at all, `ask` rejects and the login command turns that into a
|
|
27
|
+
* clear refusal.
|
|
28
|
+
*
|
|
29
|
+
* @module dsh-auth/interaction
|
|
30
|
+
*/
|
|
31
|
+
import type { AuthEvent, AuthInteraction, AuthPrompt } from '@earendil-works/pi-ai';
|
|
32
|
+
import type { AskUserQuestionItem, AskUserQuestionAnswer } from '@deepseek-ai/dsh-user-questions';
|
|
33
|
+
/** The ask surface this bridge drives (usually `ctx.userQuestions.ask`). */
|
|
34
|
+
export type AskFn = (request: {
|
|
35
|
+
questions: AskUserQuestionItem[];
|
|
36
|
+
signal?: AbortSignal;
|
|
37
|
+
}) => Promise<AskUserQuestionAnswer>;
|
|
38
|
+
/** Injectable host conveniences (tests substitute recorders). */
|
|
39
|
+
export interface QuestionBridgeHelpers {
|
|
40
|
+
openUrl?: (url: string) => boolean;
|
|
41
|
+
copyText?: (text: string) => Promise<boolean>;
|
|
42
|
+
}
|
|
43
|
+
/** Human-readable copy for one notify event. */
|
|
44
|
+
export declare function describeEvent(event: AuthEvent): string;
|
|
45
|
+
/**
|
|
46
|
+
* The bridge for one login run. At most one waiting panel exists at a time;
|
|
47
|
+
* `settle()` retires it when the flow ends (any outcome), so a completed or
|
|
48
|
+
* failed login never leaves an interactive dead end on screen.
|
|
49
|
+
*/
|
|
50
|
+
export declare class QuestionBridge implements AuthInteraction {
|
|
51
|
+
private readonly ask;
|
|
52
|
+
private readonly runAbort;
|
|
53
|
+
readonly signal: AbortSignal;
|
|
54
|
+
private readonly openUrl;
|
|
55
|
+
private readonly copy;
|
|
56
|
+
private waiting;
|
|
57
|
+
private waitingClosed;
|
|
58
|
+
constructor(ask: AskFn, runAbort: AbortController, helpers?: QuestionBridgeHelpers);
|
|
59
|
+
prompt(prompt: AuthPrompt): Promise<string>;
|
|
60
|
+
notify(event: AuthEvent): void;
|
|
61
|
+
/** Close the waiting panel (if any) and stop answering for this run. */
|
|
62
|
+
settle(): Promise<void>;
|
|
63
|
+
/**
|
|
64
|
+
* Show the waiting panel until the flow settles or the user cancels,
|
|
65
|
+
* re-asking after a copy/reopen action with a one-line status prefix.
|
|
66
|
+
*/
|
|
67
|
+
private runWaitingPanel;
|
|
68
|
+
}
|