@agimon-ai/doompi-web 0.0.1-alpha.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/LICENSE +29 -0
- package/README.md +196 -0
- package/dist/_virtual/_rolldown/runtime.cjs +1 -0
- package/dist/bin/serve.cjs +3 -0
- package/dist/bin/serve.cjs.map +1 -0
- package/dist/bin/serve.d.cts +1 -0
- package/dist/bin/serve.d.mts +1 -0
- package/dist/bin/serve.mjs +3 -0
- package/dist/bin/serve.mjs.map +1 -0
- package/dist/bundler.cjs +1 -0
- package/dist/bundler.d.cts +2 -0
- package/dist/bundler.d.mts +2 -0
- package/dist/bundler.mjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.cts +7 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +1 -0
- package/dist/src/adapters/authRoutes.cjs +2 -0
- package/dist/src/adapters/authRoutes.cjs.map +1 -0
- package/dist/src/adapters/authRoutes.mjs +2 -0
- package/dist/src/adapters/authRoutes.mjs.map +1 -0
- package/dist/src/adapters/bundledServer.cjs +2 -0
- package/dist/src/adapters/bundledServer.cjs.map +1 -0
- package/dist/src/adapters/bundledServer.mjs +2 -0
- package/dist/src/adapters/bundledServer.mjs.map +1 -0
- package/dist/src/adapters/directoryListing.cjs +2 -0
- package/dist/src/adapters/directoryListing.cjs.map +1 -0
- package/dist/src/adapters/directoryListing.mjs +2 -0
- package/dist/src/adapters/directoryListing.mjs.map +1 -0
- package/dist/src/adapters/directorySearch.cjs +3 -0
- package/dist/src/adapters/directorySearch.cjs.map +1 -0
- package/dist/src/adapters/directorySearch.mjs +3 -0
- package/dist/src/adapters/directorySearch.mjs.map +1 -0
- package/dist/src/adapters/gitStatus.cjs +2 -0
- package/dist/src/adapters/gitStatus.cjs.map +1 -0
- package/dist/src/adapters/gitStatus.mjs +2 -0
- package/dist/src/adapters/gitStatus.mjs.map +1 -0
- package/dist/src/adapters/httpServer.cjs +2 -0
- package/dist/src/adapters/httpServer.cjs.map +1 -0
- package/dist/src/adapters/httpServer.d.cts +14 -0
- package/dist/src/adapters/httpServer.d.cts.map +1 -0
- package/dist/src/adapters/httpServer.d.mts +14 -0
- package/dist/src/adapters/httpServer.d.mts.map +1 -0
- package/dist/src/adapters/httpServer.mjs +2 -0
- package/dist/src/adapters/httpServer.mjs.map +1 -0
- package/dist/src/adapters/hubProbe.cjs +2 -0
- package/dist/src/adapters/hubProbe.cjs.map +1 -0
- package/dist/src/adapters/hubProbe.mjs +2 -0
- package/dist/src/adapters/hubProbe.mjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.cjs +2 -0
- package/dist/src/adapters/packageApiProxy.cjs.map +1 -0
- package/dist/src/adapters/packageApiProxy.mjs +2 -0
- package/dist/src/adapters/packageApiProxy.mjs.map +1 -0
- package/dist/src/adapters/piHubService.cjs +2 -0
- package/dist/src/adapters/piHubService.cjs.map +1 -0
- package/dist/src/adapters/piHubService.mjs +2 -0
- package/dist/src/adapters/piHubService.mjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.cjs +2 -0
- package/dist/src/adapters/piWebSocketListener.cjs.map +1 -0
- package/dist/src/adapters/piWebSocketListener.mjs +2 -0
- package/dist/src/adapters/piWebSocketListener.mjs.map +1 -0
- package/dist/src/adapters/providerAuth.cjs +2 -0
- package/dist/src/adapters/providerAuth.cjs.map +1 -0
- package/dist/src/adapters/providerAuth.mjs +2 -0
- package/dist/src/adapters/providerAuth.mjs.map +1 -0
- package/dist/src/adapters/registryWatcher.cjs +2 -0
- package/dist/src/adapters/registryWatcher.cjs.map +1 -0
- package/dist/src/adapters/registryWatcher.mjs +2 -0
- package/dist/src/adapters/registryWatcher.mjs.map +1 -0
- package/dist/src/adapters/serverSpawner.cjs +2 -0
- package/dist/src/adapters/serverSpawner.cjs.map +1 -0
- package/dist/src/adapters/serverSpawner.mjs +2 -0
- package/dist/src/adapters/serverSpawner.mjs.map +1 -0
- package/dist/src/adapters/sessionFiles.cjs +3 -0
- package/dist/src/adapters/sessionFiles.cjs.map +1 -0
- package/dist/src/adapters/sessionFiles.mjs +3 -0
- package/dist/src/adapters/sessionFiles.mjs.map +1 -0
- package/dist/src/adapters/sessionHub.cjs +2 -0
- package/dist/src/adapters/sessionHub.cjs.map +1 -0
- package/dist/src/adapters/sessionHub.mjs +2 -0
- package/dist/src/adapters/sessionHub.mjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.cjs +2 -0
- package/dist/src/adapters/sessionSocketClient.cjs.map +1 -0
- package/dist/src/adapters/sessionSocketClient.mjs +2 -0
- package/dist/src/adapters/sessionSocketClient.mjs.map +1 -0
- package/dist/src/adapters/syncGuard.cjs +2 -0
- package/dist/src/adapters/syncGuard.cjs.map +1 -0
- package/dist/src/adapters/syncGuard.mjs +2 -0
- package/dist/src/adapters/syncGuard.mjs.map +1 -0
- package/dist/src/adapters/threadJournals.cjs +2 -0
- package/dist/src/adapters/threadJournals.cjs.map +1 -0
- package/dist/src/adapters/threadJournals.mjs +2 -0
- package/dist/src/adapters/threadJournals.mjs.map +1 -0
- package/dist/src/adapters/webBundler.cjs +2 -0
- package/dist/src/adapters/webBundler.cjs.map +1 -0
- package/dist/src/adapters/webBundler.d.cts +28 -0
- package/dist/src/adapters/webBundler.d.cts.map +1 -0
- package/dist/src/adapters/webBundler.d.mts +28 -0
- package/dist/src/adapters/webBundler.d.mts.map +1 -0
- package/dist/src/adapters/webBundler.mjs +2 -0
- package/dist/src/adapters/webBundler.mjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.cjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.cjs.map +1 -0
- package/dist/src/adapters/webHubPluginLoader.mjs +2 -0
- package/dist/src/adapters/webHubPluginLoader.mjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.cjs.map +1 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs +2 -0
- package/dist/src/adapters/webHubPlugins.generated.mjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.cjs +4 -0
- package/dist/src/adapters/webPluginGenerate.cjs.map +1 -0
- package/dist/src/adapters/webPluginGenerate.mjs +4 -0
- package/dist/src/adapters/webPluginGenerate.mjs.map +1 -0
- package/dist/src/adapters/webPluginScan.cjs +2 -0
- package/dist/src/adapters/webPluginScan.cjs.map +1 -0
- package/dist/src/adapters/webPluginScan.mjs +2 -0
- package/dist/src/adapters/webPluginScan.mjs.map +1 -0
- package/dist/src/adapters/webPluginVite.cjs +2 -0
- package/dist/src/adapters/webPluginVite.cjs.map +1 -0
- package/dist/src/adapters/webPluginVite.mjs +2 -0
- package/dist/src/adapters/webPluginVite.mjs.map +1 -0
- package/dist/src/services/directoryMatch.cjs +2 -0
- package/dist/src/services/directoryMatch.cjs.map +1 -0
- package/dist/src/services/directoryMatch.mjs +2 -0
- package/dist/src/services/directoryMatch.mjs.map +1 -0
- package/dist/src/services/fileMatch.cjs +2 -0
- package/dist/src/services/fileMatch.cjs.map +1 -0
- package/dist/src/services/fileMatch.mjs +2 -0
- package/dist/src/services/fileMatch.mjs.map +1 -0
- package/dist/src/services/fileMedia.cjs +2 -0
- package/dist/src/services/fileMedia.cjs.map +1 -0
- package/dist/src/services/fileMedia.mjs +2 -0
- package/dist/src/services/fileMedia.mjs.map +1 -0
- package/dist/src/services/frameRing.cjs +2 -0
- package/dist/src/services/frameRing.cjs.map +1 -0
- package/dist/src/services/frameRing.mjs +2 -0
- package/dist/src/services/frameRing.mjs.map +1 -0
- package/dist/src/services/journalTail.cjs +3 -0
- package/dist/src/services/journalTail.cjs.map +1 -0
- package/dist/src/services/journalTail.mjs +3 -0
- package/dist/src/services/journalTail.mjs.map +1 -0
- package/dist/src/services/loginFlow.cjs +2 -0
- package/dist/src/services/loginFlow.cjs.map +1 -0
- package/dist/src/services/loginFlow.mjs +2 -0
- package/dist/src/services/loginFlow.mjs.map +1 -0
- package/dist/src/services/registryStore.cjs +2 -0
- package/dist/src/services/registryStore.cjs.map +1 -0
- package/dist/src/services/registryStore.mjs +2 -0
- package/dist/src/services/registryStore.mjs.map +1 -0
- package/dist/src/services/retryPolicy.cjs +2 -0
- package/dist/src/services/retryPolicy.cjs.map +1 -0
- package/dist/src/services/retryPolicy.mjs +2 -0
- package/dist/src/services/retryPolicy.mjs.map +1 -0
- package/dist/src/services/serveOptions.cjs +2 -0
- package/dist/src/services/serveOptions.cjs.map +1 -0
- package/dist/src/services/serveOptions.d.cts +21 -0
- package/dist/src/services/serveOptions.d.cts.map +1 -0
- package/dist/src/services/serveOptions.d.mts +21 -0
- package/dist/src/services/serveOptions.d.mts.map +1 -0
- package/dist/src/services/serveOptions.mjs +2 -0
- package/dist/src/services/serveOptions.mjs.map +1 -0
- package/dist/src/services/sessionFraming.cjs +4 -0
- package/dist/src/services/sessionFraming.cjs.map +1 -0
- package/dist/src/services/sessionFraming.mjs +4 -0
- package/dist/src/services/sessionFraming.mjs.map +1 -0
- package/dist/src/services/sessionPresence.cjs +2 -0
- package/dist/src/services/sessionPresence.cjs.map +1 -0
- package/dist/src/services/sessionPresence.mjs +2 -0
- package/dist/src/services/sessionPresence.mjs.map +1 -0
- package/dist/src/services/staticAssets.cjs +2 -0
- package/dist/src/services/staticAssets.cjs.map +1 -0
- package/dist/src/services/staticAssets.mjs +2 -0
- package/dist/src/services/staticAssets.mjs.map +1 -0
- package/dist/src/services/webDevRoots.cjs +2 -0
- package/dist/src/services/webDevRoots.cjs.map +1 -0
- package/dist/src/services/webDevRoots.mjs +2 -0
- package/dist/src/services/webDevRoots.mjs.map +1 -0
- package/dist/src/services/webPluginManifest.cjs +2 -0
- package/dist/src/services/webPluginManifest.cjs.map +1 -0
- package/dist/src/services/webPluginManifest.mjs +2 -0
- package/dist/src/services/webPluginManifest.mjs.map +1 -0
- package/dist/src/types/auth.cjs +2 -0
- package/dist/src/types/auth.cjs.map +1 -0
- package/dist/src/types/auth.d.cts +94 -0
- package/dist/src/types/auth.d.cts.map +1 -0
- package/dist/src/types/auth.d.mts +94 -0
- package/dist/src/types/auth.d.mts.map +1 -0
- package/dist/src/types/auth.mjs +2 -0
- package/dist/src/types/auth.mjs.map +1 -0
- package/dist/src/types/bridge.d.cts +31 -0
- package/dist/src/types/bridge.d.cts.map +1 -0
- package/dist/src/types/bridge.d.mts +31 -0
- package/dist/src/types/bridge.d.mts.map +1 -0
- package/dist/src/types/hub.cjs +2 -0
- package/dist/src/types/hub.cjs.map +1 -0
- package/dist/src/types/hub.d.cts +58 -0
- package/dist/src/types/hub.d.cts.map +1 -0
- package/dist/src/types/hub.d.mts +58 -0
- package/dist/src/types/hub.d.mts.map +1 -0
- package/dist/src/types/hub.mjs +2 -0
- package/dist/src/types/hub.mjs.map +1 -0
- package/dist/src/types/media.cjs +2 -0
- package/dist/src/types/media.cjs.map +1 -0
- package/dist/src/types/media.mjs +2 -0
- package/dist/src/types/media.mjs.map +1 -0
- package/dist/src/types/registry.cjs +1 -0
- package/dist/src/types/registry.d.cts +2 -0
- package/dist/src/types/registry.d.mts +2 -0
- package/dist/src/types/registry.mjs +1 -0
- package/dist/src/types/session.cjs +2 -0
- package/dist/src/types/session.cjs.map +1 -0
- package/dist/src/types/session.d.cts +24 -0
- package/dist/src/types/session.d.cts.map +1 -0
- package/dist/src/types/session.d.mts +24 -0
- package/dist/src/types/session.d.mts.map +1 -0
- package/dist/src/types/session.mjs +2 -0
- package/dist/src/types/session.mjs.map +1 -0
- package/dist/web/apple-touch-icon.png +0 -0
- package/dist/web/assets/index-Cg52ngKv.css +2 -0
- package/dist/web/assets/index-DQO7NTIP.js +90 -0
- package/dist/web/assets/index-DQO7NTIP.js.map +1 -0
- package/dist/web/favicon-16.png +0 -0
- package/dist/web/favicon-32.png +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/favicon.svg +16 -0
- package/dist/web/icon-512.png +0 -0
- package/dist/web/index.html +17 -0
- package/llms.txt +8 -0
- package/package.json +106 -0
- package/src/adapters/authRoutes.ts +95 -0
- package/src/adapters/bundledServer.ts +64 -0
- package/src/adapters/directoryListing.ts +127 -0
- package/src/adapters/directorySearch.ts +124 -0
- package/src/adapters/gitStatus.ts +27 -0
- package/src/adapters/httpServer.ts +556 -0
- package/src/adapters/hubProbe.ts +27 -0
- package/src/adapters/packageApiProxy.ts +60 -0
- package/src/adapters/piHubService.ts +129 -0
- package/src/adapters/piWebSocketListener.ts +98 -0
- package/src/adapters/providerAuth.ts +181 -0
- package/src/adapters/registryWatcher.ts +112 -0
- package/src/adapters/serverSpawner.ts +181 -0
- package/src/adapters/sessionFiles.ts +99 -0
- package/src/adapters/sessionHub.ts +567 -0
- package/src/adapters/sessionSocketClient.ts +105 -0
- package/src/adapters/syncGuard.ts +119 -0
- package/src/adapters/threadJournals.ts +184 -0
- package/src/adapters/webBundler.ts +83 -0
- package/src/adapters/webHubPluginLoader.ts +67 -0
- package/src/adapters/webHubPlugins.generated.ts +4 -0
- package/src/adapters/webPluginGenerate.ts +172 -0
- package/src/adapters/webPluginScan.ts +62 -0
- package/src/adapters/webPluginVite.ts +51 -0
- package/src/bin/serve.ts +64 -0
- package/src/exports/bundler.ts +1 -0
- package/src/exports/index.ts +9 -0
- package/src/services/directoryMatch.ts +71 -0
- package/src/services/fileMatch.ts +37 -0
- package/src/services/fileMedia.ts +27 -0
- package/src/services/frameRing.ts +33 -0
- package/src/services/journalTail.ts +47 -0
- package/src/services/loginFlow.ts +144 -0
- package/src/services/registryStore.ts +83 -0
- package/src/services/retryPolicy.ts +15 -0
- package/src/services/serveOptions.ts +63 -0
- package/src/services/sessionFraming.ts +32 -0
- package/src/services/sessionPresence.ts +131 -0
- package/src/services/staticAssets.ts +37 -0
- package/src/services/webDevRoots.ts +36 -0
- package/src/services/webPluginManifest.ts +149 -0
- package/src/types/auth.ts +118 -0
- package/src/types/bridge.ts +41 -0
- package/src/types/hub.ts +290 -0
- package/src/types/media.ts +38 -0
- package/src/types/registry.ts +2 -0
- package/src/types/session.ts +36 -0
- package/src/web/app/Providers.tsx +46 -0
- package/src/web/app/protocolRuntime.ts +106 -0
- package/src/web/app/sessionRuntime.ts +189 -0
- package/src/web/app/webPlugins.generated.ts +4 -0
- package/src/web/components/MascotMark.tsx +46 -0
- package/src/web/components/PluginSurface.tsx +10 -0
- package/src/web/components/ToolRendererBoundary.tsx +32 -0
- package/src/web/features/activity/ActivityDock.tsx +125 -0
- package/src/web/features/connection/RefusedCard.tsx +63 -0
- package/src/web/features/dialogs/DialogOverlay.tsx +176 -0
- package/src/web/features/leader/CommandPalette.tsx +386 -0
- package/src/web/features/selection/SelectionBar.tsx +529 -0
- package/src/web/features/session/Composer.tsx +368 -0
- package/src/web/features/session/MentionPreviews.tsx +58 -0
- package/src/web/features/session/ThreadView.tsx +35 -0
- package/src/web/features/session/Timeline.tsx +382 -0
- package/src/web/features/session/ToolCard.tsx +79 -0
- package/src/web/features/sessions/NewSessionDialog.tsx +222 -0
- package/src/web/features/sessions/SessionRail.tsx +411 -0
- package/src/web/features/settings/AppearanceSettings.tsx +88 -0
- package/src/web/features/settings/LoginFlowDialog.tsx +221 -0
- package/src/web/features/settings/PluginSettings.tsx +92 -0
- package/src/web/features/settings/ProviderSettings.tsx +238 -0
- package/src/web/features/settings/SettingsMenu.tsx +32 -0
- package/src/web/features/status/TopBar.tsx +208 -0
- package/src/web/index.html +16 -0
- package/src/web/lib/authApi.ts +90 -0
- package/src/web/lib/commands.ts +61 -0
- package/src/web/lib/composition.ts +225 -0
- package/src/web/lib/fileMentions.ts +32 -0
- package/src/web/lib/hubApi.ts +86 -0
- package/src/web/lib/leaderTree.ts +159 -0
- package/src/web/lib/piTransport.ts +61 -0
- package/src/web/lib/pluginRegistry.ts +488 -0
- package/src/web/lib/pluginSlotProps.ts +50 -0
- package/src/web/lib/promptFocus.ts +25 -0
- package/src/web/lib/protocolTimeline.ts +73 -0
- package/src/web/lib/sessionModel.ts +673 -0
- package/src/web/lib/sessionSummary.ts +58 -0
- package/src/web/lib/settingsSections.ts +21 -0
- package/src/web/lib/statusLine.ts +90 -0
- package/src/web/lib/threadRenderer.ts +25 -0
- package/src/web/lib/toolMessageProps.ts +21 -0
- package/src/web/lib/transport.ts +30 -0
- package/src/web/lib/wsClient.ts +73 -0
- package/src/web/main.tsx +17 -0
- package/src/web/public/apple-touch-icon.png +0 -0
- package/src/web/public/favicon-16.png +0 -0
- package/src/web/public/favicon-32.png +0 -0
- package/src/web/public/favicon.ico +0 -0
- package/src/web/public/favicon.svg +16 -0
- package/src/web/public/icon-512.png +0 -0
- package/src/web/routes/CockpitPage.tsx +98 -0
- package/src/web/routes/SettingsPage.tsx +60 -0
- package/src/web/routes/routeTree.tsx +49 -0
- package/src/web/stores/menuStore.ts +78 -0
- package/src/web/stores/paletteStore.ts +25 -0
- package/src/web/stores/sessionStore.ts +303 -0
- package/src/web/stores/sessionsStore.ts +155 -0
- package/src/web/stores/themeStore.ts +52 -0
- package/src/web/stores/threadStore.ts +68 -0
- package/src/web/stores/transientTabsStore.ts +63 -0
- package/src/web/stores/uiStore.ts +28 -0
- package/src/web/stores/useOpenTab.ts +19 -0
- package/src/web/stores/usePluginSlotProps.ts +22 -0
- package/src/web/styles/app.css +15 -0
- package/src/web/styles/webPluginSources.generated.css +1 -0
- package/src/web/vite-env.d.ts +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface ServeOptions {
|
|
2
|
+
/** Registry override; the default is resolved by the caller. */
|
|
3
|
+
registryDir?: string;
|
|
4
|
+
/** Command launching created sessions; the caller resolves one when this is absent. */
|
|
5
|
+
spawnCommand?: string;
|
|
6
|
+
port: number;
|
|
7
|
+
host: string;
|
|
8
|
+
assetsDir?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PORT = 7433;
|
|
12
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
13
|
+
|
|
14
|
+
function requireValue(flag: string, value: string | undefined): string {
|
|
15
|
+
if (value === undefined || value.startsWith('--')) throw new Error(`${flag} needs a value.`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Parses the doompi-web command line.
|
|
21
|
+
*
|
|
22
|
+
* Every flag is an override. Bare `doompi-web` is a hub over the default
|
|
23
|
+
* registry directory on the default loopback port, which is the way it is
|
|
24
|
+
* meant to be run; nothing here is required to get a working cockpit.
|
|
25
|
+
*/
|
|
26
|
+
export function parseServeOptions(argv: readonly string[]): ServeOptions {
|
|
27
|
+
let registryDir: string | undefined;
|
|
28
|
+
let spawnCommand: string | undefined;
|
|
29
|
+
let port = DEFAULT_PORT;
|
|
30
|
+
let host = DEFAULT_HOST;
|
|
31
|
+
let assetsDir: string | undefined;
|
|
32
|
+
|
|
33
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
34
|
+
const flag = argv[index];
|
|
35
|
+
switch (flag) {
|
|
36
|
+
case '--registry-dir':
|
|
37
|
+
registryDir = requireValue(flag, argv[++index]);
|
|
38
|
+
break;
|
|
39
|
+
case '--spawn-command':
|
|
40
|
+
spawnCommand = requireValue(flag, argv[++index]);
|
|
41
|
+
break;
|
|
42
|
+
case '--port': {
|
|
43
|
+
const raw = requireValue(flag, argv[++index]);
|
|
44
|
+
const parsed = Number.parseInt(raw, 10);
|
|
45
|
+
if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
|
|
46
|
+
throw new Error(`--port expects a port number, received "${raw}".`);
|
|
47
|
+
}
|
|
48
|
+
port = parsed;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
case '--host':
|
|
52
|
+
host = requireValue(flag, argv[++index]);
|
|
53
|
+
break;
|
|
54
|
+
case '--assets':
|
|
55
|
+
assetsDir = requireValue(flag, argv[++index]);
|
|
56
|
+
break;
|
|
57
|
+
default:
|
|
58
|
+
throw new Error(`Unknown option "${flag}".`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { registryDir, spawnCommand, port, host, assetsDir };
|
|
63
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SessionFrame } from '../types/session.ts';
|
|
2
|
+
|
|
3
|
+
const NEWLINE = '\n';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits a byte stream into newline-delimited JSON frames.
|
|
7
|
+
*
|
|
8
|
+
* Stateful across chunks because a socket read can land mid-frame, which is the
|
|
9
|
+
* normal case for streamed agent output rather than an edge case.
|
|
10
|
+
*/
|
|
11
|
+
export function createFrameDecoder(): (chunk: string) => SessionFrame[] {
|
|
12
|
+
let buffered = '';
|
|
13
|
+
return (chunk: string): SessionFrame[] => {
|
|
14
|
+
buffered += chunk;
|
|
15
|
+
const parts = buffered.split(NEWLINE);
|
|
16
|
+
buffered = parts.pop() ?? '';
|
|
17
|
+
const frames: SessionFrame[] = [];
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
const line = part.trim();
|
|
20
|
+
if (!line) continue;
|
|
21
|
+
const parsed: unknown = JSON.parse(line);
|
|
22
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
23
|
+
frames.push(parsed as SessionFrame);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return frames;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function encodeFrame(frame: SessionFrame): string {
|
|
31
|
+
return `${JSON.stringify(frame)}${NEWLINE}`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { SessionPhase } from '../types/hub.ts';
|
|
2
|
+
import type { SessionFrame } from '../types/session.ts';
|
|
3
|
+
|
|
4
|
+
const DIALOG_METHODS = ['select', 'confirm', 'input', 'editor'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What one session's frame stream says it is doing.
|
|
8
|
+
*
|
|
9
|
+
* Reduced by the hub for every session it holds, attached or not focused, so
|
|
10
|
+
* the rail can describe sessions nobody is looking at. The reducer returns the
|
|
11
|
+
* SAME object when a frame changes nothing it reports; callers use that
|
|
12
|
+
* reference equality to skip redundant summary pushes.
|
|
13
|
+
*/
|
|
14
|
+
export interface SessionPresence {
|
|
15
|
+
phase: SessionPhase;
|
|
16
|
+
/** ISO 8601 timestamp of the last phase change. */
|
|
17
|
+
phaseSince: string;
|
|
18
|
+
/** ISO 8601 timestamp of the last change to any reported fact. */
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
pendingMessageCount: number;
|
|
21
|
+
everPrompted: boolean;
|
|
22
|
+
awaitingInput: boolean;
|
|
23
|
+
lastSettledAt?: string;
|
|
24
|
+
/** Name reported by get_state, fresher than the registry record when set. */
|
|
25
|
+
sessionName?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function initialPresence(now: string): SessionPresence {
|
|
29
|
+
return {
|
|
30
|
+
phase: 'idle',
|
|
31
|
+
phaseSince: now,
|
|
32
|
+
updatedAt: now,
|
|
33
|
+
pendingMessageCount: 0,
|
|
34
|
+
everPrompted: false,
|
|
35
|
+
awaitingInput: false,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
40
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withPhase(presence: SessionPresence, phase: SessionPhase, now: string): SessionPresence {
|
|
44
|
+
if (presence.phase === phase) return presence;
|
|
45
|
+
return { ...presence, phase, phaseSince: now, updatedAt: now };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyState(presence: SessionPresence, data: Record<string, unknown>, now: string): SessionPresence {
|
|
49
|
+
let next = presence;
|
|
50
|
+
const phase: SessionPhase = data.isCompacting === true ? 'compaction' : data.isStreaming === true ? 'turn' : 'idle';
|
|
51
|
+
next = withPhase(next, phase, now);
|
|
52
|
+
const pending = typeof data.pendingMessageCount === 'number' ? data.pendingMessageCount : next.pendingMessageCount;
|
|
53
|
+
if (pending !== next.pendingMessageCount) next = { ...next, pendingMessageCount: pending, updatedAt: now };
|
|
54
|
+
const name = typeof data.sessionName === 'string' && data.sessionName !== '' ? data.sessionName : undefined;
|
|
55
|
+
if (name !== undefined && name !== next.sessionName) next = { ...next, sessionName: name, updatedAt: now };
|
|
56
|
+
return next;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Folds one agent frame into the presence.
|
|
61
|
+
*
|
|
62
|
+
* Pure and total like reduceSession: an unrecognised frame changes nothing.
|
|
63
|
+
*/
|
|
64
|
+
export function reducePresence(presence: SessionPresence, frame: SessionFrame, now: string): SessionPresence {
|
|
65
|
+
switch (frame.type) {
|
|
66
|
+
case 'replay':
|
|
67
|
+
return isRecord(frame.frame) ? reducePresence(presence, frame.frame, now) : presence;
|
|
68
|
+
|
|
69
|
+
case 'agent_start': {
|
|
70
|
+
const started = withPhase(presence, 'turn', now);
|
|
71
|
+
if (!started.awaitingInput) return started;
|
|
72
|
+
return { ...started, awaitingInput: false, updatedAt: now };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
case 'agent_settled': {
|
|
76
|
+
const settled = withPhase(presence, 'idle', now);
|
|
77
|
+
return { ...settled, awaitingInput: false, lastSettledAt: now, updatedAt: now };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
case 'extension_ui_request': {
|
|
81
|
+
if (typeof frame.method !== 'string' || !DIALOG_METHODS.includes(frame.method)) return presence;
|
|
82
|
+
if (presence.awaitingInput) return presence;
|
|
83
|
+
return { ...presence, awaitingInput: true, updatedAt: now };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'response':
|
|
87
|
+
return frame.command === 'get_state' && isRecord(frame.data) ? applyState(presence, frame.data, now) : presence;
|
|
88
|
+
|
|
89
|
+
default:
|
|
90
|
+
return presence;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Folds one entry restored from the session's journal.
|
|
96
|
+
*
|
|
97
|
+
* A session outlives every hub that watches it, so "has this ever been
|
|
98
|
+
* prompted" cannot be answered from what this process happened to see: a hub
|
|
99
|
+
* that restarts, or meets a session first driven from the TUI, saw nothing. A
|
|
100
|
+
* journalled user message is the proof, and without it a session carrying an
|
|
101
|
+
* hour of work introduces itself in the rail as fresh.
|
|
102
|
+
*/
|
|
103
|
+
export function presenceAfterRestoredEntry(
|
|
104
|
+
presence: SessionPresence,
|
|
105
|
+
entry: Record<string, unknown>,
|
|
106
|
+
now: string,
|
|
107
|
+
): SessionPresence {
|
|
108
|
+
if (presence.everPrompted || entry.type !== 'message') return presence;
|
|
109
|
+
const message = entry.message;
|
|
110
|
+
if (typeof message !== 'object' || message === null) return presence;
|
|
111
|
+
if ((message as { role?: unknown }).role !== 'user') return presence;
|
|
112
|
+
return { ...presence, everPrompted: true, updatedAt: now };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Folds one page command heading to the agent.
|
|
117
|
+
*
|
|
118
|
+
* The prompt direction carries facts the event stream does not repeat: that
|
|
119
|
+
* the session has ever been prompted, and that a pending dialog was answered.
|
|
120
|
+
*/
|
|
121
|
+
export function presenceAfterCommand(presence: SessionPresence, frame: SessionFrame, now: string): SessionPresence {
|
|
122
|
+
if (frame.type === 'prompt' || frame.type === 'steer' || frame.type === 'follow_up') {
|
|
123
|
+
if (presence.everPrompted) return presence;
|
|
124
|
+
return { ...presence, everPrompted: true, updatedAt: now };
|
|
125
|
+
}
|
|
126
|
+
if (frame.type === 'extension_ui_response') {
|
|
127
|
+
if (!presence.awaitingInput) return presence;
|
|
128
|
+
return { ...presence, awaitingInput: false, updatedAt: now };
|
|
129
|
+
}
|
|
130
|
+
return presence;
|
|
131
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
const CONTENT_TYPES: Readonly<Record<string, string>> = {
|
|
4
|
+
'.css': 'text/css; charset=utf-8',
|
|
5
|
+
'.html': 'text/html; charset=utf-8',
|
|
6
|
+
'.ico': 'image/x-icon',
|
|
7
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
8
|
+
'.json': 'application/json; charset=utf-8',
|
|
9
|
+
'.map': 'application/json; charset=utf-8',
|
|
10
|
+
'.png': 'image/png',
|
|
11
|
+
'.svg': 'image/svg+xml',
|
|
12
|
+
'.woff2': 'font/woff2',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function contentTypeFor(filePath: string): string {
|
|
16
|
+
return CONTENT_TYPES[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Maps a request path to a file inside the asset root.
|
|
21
|
+
*
|
|
22
|
+
* Returns undefined when the result would escape the root, so an encoded
|
|
23
|
+
* traversal cannot read files the bundle was never meant to publish.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveAssetPath(assetsDir: string, requestPath: string): string | undefined {
|
|
26
|
+
let decoded: string;
|
|
27
|
+
try {
|
|
28
|
+
decoded = decodeURIComponent(requestPath);
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (decoded.includes('\0')) return undefined;
|
|
33
|
+
const root = path.resolve(assetsDir);
|
|
34
|
+
const candidate = path.resolve(root, `.${path.posix.normalize(decoded)}`);
|
|
35
|
+
if (candidate !== root && !candidate.startsWith(`${root}${path.sep}`)) return undefined;
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the cockpit's dev server finds the plugin packages to serve with hot
|
|
3
|
+
* reload. Pure: the adapter hands in the environment value and the roots file
|
|
4
|
+
* the last `doompi sync` wrote next to its bundle; this only decides.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** A delimiter-separated list of plugin package roots; wins over the synced roots file. */
|
|
8
|
+
export const PLUGIN_ROOTS_ENV = 'DOOMPI_WEB_PLUGIN_ROOTS';
|
|
9
|
+
/** The file `doompi sync` writes beside its bundle, listing the package roots it bundled. */
|
|
10
|
+
export const PLUGIN_ROOTS_FILE = 'pluginRoots.json';
|
|
11
|
+
|
|
12
|
+
export interface DevPluginRootsInput {
|
|
13
|
+
/** The raw environment value, undefined when unset. */
|
|
14
|
+
envValue: string | undefined;
|
|
15
|
+
/** The roots file's text, undefined when no synced bundle exists. */
|
|
16
|
+
rootsFileText: string | undefined;
|
|
17
|
+
/** The platform's path list delimiter (':' on POSIX, ';' on Windows). */
|
|
18
|
+
delimiter: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The plugin roots the dev server should serve: the environment list, else the synced list, else none. */
|
|
22
|
+
export function devPluginRoots({ envValue, rootsFileText, delimiter }: DevPluginRootsInput): string[] {
|
|
23
|
+
if (envValue !== undefined && envValue.trim() !== '') {
|
|
24
|
+
return envValue
|
|
25
|
+
.split(delimiter)
|
|
26
|
+
.map((root) => root.trim())
|
|
27
|
+
.filter((root) => root !== '');
|
|
28
|
+
}
|
|
29
|
+
if (rootsFileText === undefined) return [];
|
|
30
|
+
try {
|
|
31
|
+
const parsed: unknown = JSON.parse(rootsFileText);
|
|
32
|
+
return Array.isArray(parsed) ? parsed.filter((root): root is string => typeof root === 'string') : [];
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The doompiWeb manifest vocabulary: how a package declares its web plugin in
|
|
3
|
+
* package.json, validated and ordered without touching the filesystem. The
|
|
4
|
+
* scan adapter feeds this from real manifests; the sync bundler feeds it from
|
|
5
|
+
* the installed composition's package roots.
|
|
6
|
+
*
|
|
7
|
+
* Plugins are independent: a manifest names no other plugin, and the order
|
|
8
|
+
* plugins install in is only a tiebreak (registrationOrder, then pluginId),
|
|
9
|
+
* because every relation between two plugins resolves by name once all of
|
|
10
|
+
* them are installed. A collision between two packages is a notice for the
|
|
11
|
+
* sync log and the first package keeps the name; only a malformed block is an
|
|
12
|
+
* error, which the scanner turns into a notice for that package alone.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
16
|
+
|
|
17
|
+
/** The order a plugin installs at when its manifest names none. */
|
|
18
|
+
export const DEFAULT_REGISTRATION_ORDER = 1000;
|
|
19
|
+
|
|
20
|
+
export interface WebPluginEntryDeclaration {
|
|
21
|
+
/** Package-relative ./path to the source entry. */
|
|
22
|
+
entry: string;
|
|
23
|
+
/** Package-relative ./path to the built hub entry; required for non-host hub blocks. */
|
|
24
|
+
dist?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DeclaredWebPlugin {
|
|
28
|
+
pluginId: string;
|
|
29
|
+
registrationOrder: number;
|
|
30
|
+
channels: string[];
|
|
31
|
+
packageDir: string;
|
|
32
|
+
packageName: string;
|
|
33
|
+
isHost: boolean;
|
|
34
|
+
client: WebPluginEntryDeclaration;
|
|
35
|
+
hub?: WebPluginEntryDeclaration;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class WebPluginManifestError extends Error {
|
|
39
|
+
constructor(packageDir: string, message: string) {
|
|
40
|
+
super(`doompiWeb manifest in ${packageDir}: ${message}`);
|
|
41
|
+
this.name = 'WebPluginManifestError';
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
46
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeEntry(packageDir: string, isHost: boolean, kind: string, value: unknown): WebPluginEntryDeclaration {
|
|
50
|
+
const raw = typeof value === 'string' ? { entry: value } : value;
|
|
51
|
+
if (!isRecord(raw)) throw new WebPluginManifestError(packageDir, `${kind} must be a path or {entry, dist}.`);
|
|
52
|
+
const { entry, dist } = raw;
|
|
53
|
+
if (typeof entry !== 'string' || !entry.startsWith('./') || entry.includes('..')) {
|
|
54
|
+
throw new WebPluginManifestError(packageDir, `${kind}.entry must be a package-relative ./path with no '..'.`);
|
|
55
|
+
}
|
|
56
|
+
if (dist !== undefined && (typeof dist !== 'string' || !dist.startsWith('./') || dist.includes('..'))) {
|
|
57
|
+
throw new WebPluginManifestError(packageDir, `${kind}.dist must be a package-relative ./path with no '..'.`);
|
|
58
|
+
}
|
|
59
|
+
if (kind === 'hub' && !isHost && typeof dist !== 'string') {
|
|
60
|
+
throw new WebPluginManifestError(
|
|
61
|
+
packageDir,
|
|
62
|
+
'hub.dist is required for a non-host package: the hub imports the built entry the package ships.',
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
return { entry, ...(typeof dist === 'string' ? { dist } : {}) };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The doompiWeb value normalized to a list: one plugin object or an array of them. */
|
|
69
|
+
export function pluginBlocksOf(manifest: Record<string, unknown>): unknown[] {
|
|
70
|
+
if (manifest.doompiWeb === undefined) return [];
|
|
71
|
+
return Array.isArray(manifest.doompiWeb) ? manifest.doompiWeb : [manifest.doompiWeb];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Validates one manifest's blocks into declared plugins; throws WebPluginManifestError on bad shape. */
|
|
75
|
+
export function declaredPluginsOf(
|
|
76
|
+
packageDir: string,
|
|
77
|
+
manifest: Record<string, unknown>,
|
|
78
|
+
isHost: boolean,
|
|
79
|
+
): DeclaredWebPlugin[] {
|
|
80
|
+
const plugins: DeclaredWebPlugin[] = [];
|
|
81
|
+
for (const block of pluginBlocksOf(manifest)) {
|
|
82
|
+
if (!isRecord(block)) throw new WebPluginManifestError(packageDir, 'each block must be an object.');
|
|
83
|
+
const { pluginId, registrationOrder = DEFAULT_REGISTRATION_ORDER, channels = [], client, hub } = block;
|
|
84
|
+
if (typeof pluginId !== 'string' || !PLUGIN_ID_PATTERN.test(pluginId)) {
|
|
85
|
+
throw new WebPluginManifestError(packageDir, `pluginId '${String(pluginId)}' must be kebab-case.`);
|
|
86
|
+
}
|
|
87
|
+
if (!Number.isInteger(registrationOrder) || (registrationOrder as number) < 0) {
|
|
88
|
+
throw new WebPluginManifestError(packageDir, `'${pluginId}' needs a non-negative integer registrationOrder.`);
|
|
89
|
+
}
|
|
90
|
+
if (!Array.isArray(channels) || channels.some((channel) => typeof channel !== 'string' || channel === '')) {
|
|
91
|
+
throw new WebPluginManifestError(packageDir, `'${pluginId}' channels must be non-empty strings.`);
|
|
92
|
+
}
|
|
93
|
+
plugins.push({
|
|
94
|
+
pluginId,
|
|
95
|
+
registrationOrder: registrationOrder as number,
|
|
96
|
+
channels: channels as string[],
|
|
97
|
+
packageDir,
|
|
98
|
+
packageName: typeof manifest.name === 'string' ? manifest.name : packageDir,
|
|
99
|
+
isHost,
|
|
100
|
+
client: normalizeEntry(packageDir, isHost, 'client', client),
|
|
101
|
+
...(hub === undefined ? {} : { hub: normalizeEntry(packageDir, isHost, 'hub', hub) }),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return plugins;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const byInstallOrder = (left: DeclaredWebPlugin, right: DeclaredWebPlugin): number =>
|
|
108
|
+
left.registrationOrder - right.registrationOrder ||
|
|
109
|
+
left.pluginId.localeCompare(right.pluginId) ||
|
|
110
|
+
left.packageDir.localeCompare(right.packageDir);
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The deterministic install order, (registrationOrder, pluginId), with the
|
|
114
|
+
* cross-package collisions resolved the way the client registry resolves
|
|
115
|
+
* them: the first plugin keeps a shared pluginId and the later package is
|
|
116
|
+
* dropped with a notice; a channel two kept plugins both declare is noticed
|
|
117
|
+
* here and settled first-wins by the client and the hub loader.
|
|
118
|
+
*/
|
|
119
|
+
export function orderDeclaredPlugins(
|
|
120
|
+
plugins: readonly DeclaredWebPlugin[],
|
|
121
|
+
onNotice: (message: string) => void = () => undefined,
|
|
122
|
+
): DeclaredWebPlugin[] {
|
|
123
|
+
const sorted = [...plugins].sort(byInstallOrder);
|
|
124
|
+
const byId = new Map<string, DeclaredWebPlugin>();
|
|
125
|
+
const channelOwners = new Map<string, string>();
|
|
126
|
+
const kept: DeclaredWebPlugin[] = [];
|
|
127
|
+
for (const plugin of sorted) {
|
|
128
|
+
const holder = byId.get(plugin.pluginId);
|
|
129
|
+
if (holder !== undefined) {
|
|
130
|
+
onNotice(
|
|
131
|
+
`web plugin '${plugin.pluginId}' from ${plugin.packageDir} is skipped: ${holder.packageDir} already declares it.`,
|
|
132
|
+
);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
byId.set(plugin.pluginId, plugin);
|
|
136
|
+
kept.push(plugin);
|
|
137
|
+
for (const channel of plugin.channels) {
|
|
138
|
+
const owner = channelOwners.get(channel);
|
|
139
|
+
if (owner !== undefined) {
|
|
140
|
+
onNotice(
|
|
141
|
+
`web plugin '${plugin.pluginId}' channel '${channel}' is already claimed by '${owner}'; the first keeps it.`,
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
channelOwners.set(channel, plugin.pluginId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return kept;
|
|
149
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire vocabulary for provider authentication, shared by the hub routes and
|
|
3
|
+
* the settings page.
|
|
4
|
+
*
|
|
5
|
+
* The hub keeps one Pi ModelRuntime over the same auth.json the sessions
|
|
6
|
+
* read, so a login here is a login for every session on this machine. Prompt
|
|
7
|
+
* and event shapes restate pi-ai's AuthPrompt and AuthEvent so neither the
|
|
8
|
+
* flow service nor the page needs a pi import; the abort signal never leaves
|
|
9
|
+
* the hub.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** REST endpoint listing providers with their auth state; DELETE /:providerId signs out. */
|
|
13
|
+
export const AUTH_PROVIDERS_API_ROUTE = '/api/auth/providers';
|
|
14
|
+
/** REST endpoint for login flows: POST starts one, GET /:flowId polls it, POST /:flowId/answer answers its prompt, DELETE /:flowId cancels. */
|
|
15
|
+
export const AUTH_LOGINS_API_ROUTE = '/api/auth/logins';
|
|
16
|
+
|
|
17
|
+
export type AuthMethodType = 'api_key' | 'oauth';
|
|
18
|
+
|
|
19
|
+
export interface ProviderAuthMethod {
|
|
20
|
+
type: AuthMethodType;
|
|
21
|
+
/** What the provider calls this method: "Anthropic API key", "Anthropic (Claude Pro/Max)". */
|
|
22
|
+
label: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ProviderAuthSummary {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
/** Login methods the provider offers; ambient-only providers list none. */
|
|
29
|
+
methods: ProviderAuthMethod[];
|
|
30
|
+
/**
|
|
31
|
+
* Present once the provider has working auth: how, and where the credential
|
|
32
|
+
* comes from ("stored", or the environment variable that holds the key).
|
|
33
|
+
*/
|
|
34
|
+
authenticated?: { type: AuthMethodType; source?: string };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface LoginSelectOption {
|
|
38
|
+
id: string;
|
|
39
|
+
label: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** pi-ai's AuthPrompt, restated. `signal` cancels this one question, not the flow. */
|
|
44
|
+
export type LoginPrompt = { signal?: AbortSignal } & (
|
|
45
|
+
| { type: 'text'; message: string; placeholder?: string }
|
|
46
|
+
| { type: 'secret'; message: string; placeholder?: string }
|
|
47
|
+
| { type: 'select'; message: string; options: readonly LoginSelectOption[] }
|
|
48
|
+
| { type: 'manual_code'; message: string; placeholder?: string }
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
/** The question the page answers: a prompt with an id and without its signal. */
|
|
52
|
+
export interface LoginPromptView {
|
|
53
|
+
id: string;
|
|
54
|
+
type: LoginPrompt['type'];
|
|
55
|
+
message: string;
|
|
56
|
+
placeholder?: string;
|
|
57
|
+
/** Present for select prompts. */
|
|
58
|
+
options?: LoginSelectOption[];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** pi-ai's AuthEvent, restated. */
|
|
62
|
+
export type LoginEvent =
|
|
63
|
+
| { type: 'info'; message: string; links?: readonly { url: string; label?: string }[] }
|
|
64
|
+
| { type: 'auth_url'; url: string; instructions?: string }
|
|
65
|
+
| {
|
|
66
|
+
type: 'device_code';
|
|
67
|
+
userCode: string;
|
|
68
|
+
verificationUri: string;
|
|
69
|
+
intervalSeconds?: number;
|
|
70
|
+
expiresInSeconds?: number;
|
|
71
|
+
}
|
|
72
|
+
| { type: 'progress'; message: string };
|
|
73
|
+
|
|
74
|
+
export type LoginFlowStatus = 'running' | 'succeeded' | 'failed' | 'cancelled';
|
|
75
|
+
|
|
76
|
+
export interface LoginFlowSnapshot {
|
|
77
|
+
id: string;
|
|
78
|
+
providerId: string;
|
|
79
|
+
providerName: string;
|
|
80
|
+
type: AuthMethodType;
|
|
81
|
+
status: LoginFlowStatus;
|
|
82
|
+
events: LoginEvent[];
|
|
83
|
+
/** The question the flow is waiting on, while there is one. */
|
|
84
|
+
prompt?: LoginPromptView;
|
|
85
|
+
/** Why the flow failed, once it has. */
|
|
86
|
+
error?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** pi-ai's AuthInteraction: what a provider's login needs from its host. */
|
|
90
|
+
export interface LoginInteraction {
|
|
91
|
+
signal: AbortSignal;
|
|
92
|
+
prompt(prompt: LoginPrompt): Promise<string>;
|
|
93
|
+
notify(event: LoginEvent): void;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface AuthRuntimeProvider {
|
|
97
|
+
readonly id: string;
|
|
98
|
+
readonly name: string;
|
|
99
|
+
readonly auth: {
|
|
100
|
+
/** Absent `login` means ambient-only (env vars, AWS profiles): nothing to sign in to. */
|
|
101
|
+
apiKey?: { name: string; login?: unknown };
|
|
102
|
+
oauth?: { name: string; loginLabel?: string };
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The slice of Pi's ModelRuntime the hub relies on, so a test can stand in a
|
|
108
|
+
* fake and the adapter never types against the whole agent.
|
|
109
|
+
*/
|
|
110
|
+
export interface AuthRuntime {
|
|
111
|
+
getProviders(): readonly AuthRuntimeProvider[];
|
|
112
|
+
getProviderAuthStatus(providerId: string): { configured: boolean; source?: string; label?: string };
|
|
113
|
+
isUsingOAuth(providerId: string): boolean;
|
|
114
|
+
/** Re-reads models.json and every provider's auth state without touching the network. */
|
|
115
|
+
refresh(options: { allowNetwork: boolean }): Promise<unknown>;
|
|
116
|
+
login(providerId: string, type: AuthMethodType, interaction: LoginInteraction): Promise<unknown>;
|
|
117
|
+
logout(providerId: string, options?: { signal?: AbortSignal }): Promise<void>;
|
|
118
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AuthRuntime } from './auth.ts';
|
|
2
|
+
import type { BridgeStatusFrame, SessionFrame } from './session.ts';
|
|
3
|
+
|
|
4
|
+
/** One live attachment to a doompi-server socket. */
|
|
5
|
+
export interface SessionAttachment {
|
|
6
|
+
/** Forwards a command frame to the agent. Ignored once closed. */
|
|
7
|
+
send(frame: SessionFrame): void;
|
|
8
|
+
close(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AttachmentHandlers {
|
|
12
|
+
onFrame(frame: SessionFrame): void;
|
|
13
|
+
onStatus(status: BridgeStatusFrame): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AttachOptions {
|
|
17
|
+
socketPath: string;
|
|
18
|
+
token: string;
|
|
19
|
+
handlers: AttachmentHandlers;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WebServerOptions {
|
|
23
|
+
port: number;
|
|
24
|
+
/** Loopback by default; the socket token is not a browser credential. */
|
|
25
|
+
host?: string;
|
|
26
|
+
/** Directory holding the built SPA. Defaults to the bundled dist/web. */
|
|
27
|
+
assetsDir?: string;
|
|
28
|
+
onNotice?: (message: string) => void;
|
|
29
|
+
/** Watch this registry directory for running sessions. */
|
|
30
|
+
registryDir: string;
|
|
31
|
+
/** Command launching created sessions; overridable so tests can stand in a fake. */
|
|
32
|
+
spawnCommand?: string;
|
|
33
|
+
/** Provider auth runtime; overridable so tests can stand in a fake for Pi's ModelRuntime. */
|
|
34
|
+
authRuntime?: () => Promise<AuthRuntime>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface WebServer {
|
|
38
|
+
readonly url: string;
|
|
39
|
+
readonly port: number;
|
|
40
|
+
close(): Promise<void>;
|
|
41
|
+
}
|