@decocms/mesh-sdk 1.2.1 → 1.2.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/package.json +6 -4
- package/src/context/index.ts +6 -1
- package/src/context/project-context.tsx +78 -29
- package/src/hooks/index.ts +7 -0
- package/src/hooks/use-collections.ts +160 -51
- package/src/hooks/use-connection.ts +39 -4
- package/src/hooks/use-mcp-client.ts +55 -2
- package/src/hooks/use-mcp-prompts.ts +16 -6
- package/src/hooks/use-mcp-resources.ts +15 -5
- package/src/index.ts +82 -3
- package/src/lib/bridge-transport.test.ts +368 -0
- package/src/lib/bridge-transport.ts +434 -0
- package/src/lib/constants.ts +113 -10
- package/src/lib/default-model.ts +96 -0
- package/src/lib/mcp-oauth.ts +80 -9
- package/src/lib/query-keys.ts +1 -0
- package/src/lib/server-client-bridge.ts +146 -0
- package/src/lib/usage.test.ts +163 -0
- package/src/lib/usage.ts +161 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/plugin-context-provider.tsx +99 -0
- package/src/plugins/topbar-portal.tsx +118 -0
- package/src/types/ai-providers.ts +68 -0
- package/src/types/connection.ts +38 -20
- package/src/types/decopilot-events.ts +128 -0
- package/src/types/index.ts +30 -1
- package/src/types/virtual-mcp.ts +107 -109
package/src/lib/usage.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Usage utilities for extracting cost and token stats from AI provider metadata.
|
|
3
|
+
*
|
|
4
|
+
* Supports provider-specific cost extraction (e.g., OpenRouter)
|
|
5
|
+
* and aggregation of usage across messages or streaming steps.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Types
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
export interface UsageData {
|
|
13
|
+
inputTokens?: number;
|
|
14
|
+
outputTokens?: number;
|
|
15
|
+
reasoningTokens?: number;
|
|
16
|
+
totalTokens?: number;
|
|
17
|
+
providerMetadata?: {
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UsageStats {
|
|
23
|
+
inputTokens: number;
|
|
24
|
+
outputTokens: number;
|
|
25
|
+
reasoningTokens: number;
|
|
26
|
+
totalTokens: number;
|
|
27
|
+
cost: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ProviderCostExtractor = (
|
|
31
|
+
providerMetadata: NonNullable<UsageData["providerMetadata"]>,
|
|
32
|
+
) => number | null;
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Provider-specific cost extractors
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Registry of provider-specific cost extractors.
|
|
40
|
+
* Each extractor attempts to get the cost from provider metadata.
|
|
41
|
+
*/
|
|
42
|
+
const PROVIDER_COST_EXTRACTORS: Record<string, ProviderCostExtractor> = {
|
|
43
|
+
openrouter: (providerMetadata) => {
|
|
44
|
+
const openrouter = providerMetadata?.openrouter;
|
|
45
|
+
if (
|
|
46
|
+
typeof openrouter === "object" &&
|
|
47
|
+
openrouter !== null &&
|
|
48
|
+
"usage" in openrouter &&
|
|
49
|
+
typeof openrouter.usage === "object" &&
|
|
50
|
+
openrouter.usage !== null &&
|
|
51
|
+
"cost" in openrouter.usage &&
|
|
52
|
+
typeof openrouter.usage.cost === "number"
|
|
53
|
+
) {
|
|
54
|
+
return openrouter.usage.cost;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Cost extraction
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extract cost from usage metadata by checking all known provider formats.
|
|
66
|
+
*/
|
|
67
|
+
export function getCostFromUsage(usage: UsageData | null | undefined): number {
|
|
68
|
+
if (!usage?.providerMetadata) {
|
|
69
|
+
return 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const extractor of Object.values(PROVIDER_COST_EXTRACTORS)) {
|
|
73
|
+
const cost = extractor(usage.providerMetadata);
|
|
74
|
+
if (cost !== null) {
|
|
75
|
+
return cost;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Provider metadata sanitization
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
const ALLOWED_PROVIDER_FIELDS = ["usage", "cost", "model"] as const;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Sanitize provider metadata to prevent leaking sensitive data.
|
|
90
|
+
* Only allows whitelisted fields: usage, cost, model.
|
|
91
|
+
*/
|
|
92
|
+
export function sanitizeProviderMetadata(
|
|
93
|
+
metadata: Record<string, unknown> | undefined,
|
|
94
|
+
): Record<string, unknown> | undefined {
|
|
95
|
+
if (!metadata) return undefined;
|
|
96
|
+
|
|
97
|
+
const sanitized: Record<string, unknown> = {};
|
|
98
|
+
for (const provider in metadata) {
|
|
99
|
+
const providerData = metadata[provider];
|
|
100
|
+
if (typeof providerData === "object" && providerData !== null) {
|
|
101
|
+
const safeData: Record<string, unknown> = {};
|
|
102
|
+
for (const field of ALLOWED_PROVIDER_FIELDS) {
|
|
103
|
+
if (field in providerData) {
|
|
104
|
+
safeData[field] = (providerData as Record<string, unknown>)[field];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
sanitized[provider] = safeData;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return Object.keys(sanitized).length > 0 ? sanitized : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ============================================================================
|
|
114
|
+
// Usage accumulation
|
|
115
|
+
// ============================================================================
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create an empty UsageStats object.
|
|
119
|
+
*/
|
|
120
|
+
export function emptyUsageStats(): UsageStats {
|
|
121
|
+
return {
|
|
122
|
+
inputTokens: 0,
|
|
123
|
+
outputTokens: 0,
|
|
124
|
+
reasoningTokens: 0,
|
|
125
|
+
totalTokens: 0,
|
|
126
|
+
cost: 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Accumulate a step's usage into an existing UsageStats total.
|
|
132
|
+
* Returns a new UsageStats object (immutable).
|
|
133
|
+
*/
|
|
134
|
+
export function addUsage(
|
|
135
|
+
accumulated: UsageStats,
|
|
136
|
+
stepUsage: UsageData | null | undefined,
|
|
137
|
+
): UsageStats {
|
|
138
|
+
if (!stepUsage) return accumulated;
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
inputTokens: accumulated.inputTokens + (stepUsage.inputTokens ?? 0),
|
|
142
|
+
outputTokens: accumulated.outputTokens + (stepUsage.outputTokens ?? 0),
|
|
143
|
+
reasoningTokens:
|
|
144
|
+
accumulated.reasoningTokens + (stepUsage.reasoningTokens ?? 0),
|
|
145
|
+
totalTokens: accumulated.totalTokens + (stepUsage.totalTokens ?? 0),
|
|
146
|
+
cost: accumulated.cost + getCostFromUsage(stepUsage),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Calculate aggregated usage stats from an array of messages.
|
|
152
|
+
* Each message is expected to have an optional `metadata.usage` field.
|
|
153
|
+
*/
|
|
154
|
+
export function calculateUsageStats(
|
|
155
|
+
messages: Array<{ metadata?: { usage?: UsageData } }>,
|
|
156
|
+
): UsageStats {
|
|
157
|
+
return messages.reduce<UsageStats>(
|
|
158
|
+
(acc, message) => addUsage(acc, message.metadata?.usage),
|
|
159
|
+
emptyUsageStats(),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Plugin context provider and hook
|
|
2
|
+
export {
|
|
3
|
+
PluginContextProvider,
|
|
4
|
+
usePluginContext,
|
|
5
|
+
type PluginContextProviderProps,
|
|
6
|
+
type UsePluginContextOptions,
|
|
7
|
+
} from "./plugin-context-provider";
|
|
8
|
+
|
|
9
|
+
// Topbar portal system
|
|
10
|
+
export {
|
|
11
|
+
TopbarPortal,
|
|
12
|
+
TopbarPortalProvider,
|
|
13
|
+
useTopbarPortalTargets,
|
|
14
|
+
type TopbarSide,
|
|
15
|
+
} from "./topbar-portal";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Context Provider
|
|
3
|
+
*
|
|
4
|
+
* React context provider and hook for accessing plugin context.
|
|
5
|
+
* Moved from @decocms/bindings to @decocms/mesh-sdk to consolidate
|
|
6
|
+
* all plugin-facing React components in one package.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { createContext, useContext, type ReactNode } from "react";
|
|
10
|
+
import type {
|
|
11
|
+
Binder,
|
|
12
|
+
PluginContext,
|
|
13
|
+
PluginContextPartial,
|
|
14
|
+
} from "@decocms/bindings";
|
|
15
|
+
|
|
16
|
+
// Internal context stores the partial version (nullable connection fields)
|
|
17
|
+
// The hook return type depends on the options passed
|
|
18
|
+
const PluginContextInternal = createContext<PluginContextPartial | null>(null);
|
|
19
|
+
|
|
20
|
+
export interface PluginContextProviderProps<TBinding extends Binder> {
|
|
21
|
+
value: PluginContext<TBinding> | PluginContextPartial<TBinding>;
|
|
22
|
+
children: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Provider component for plugin context.
|
|
27
|
+
* Used by the mesh app layout to provide context to plugin routes.
|
|
28
|
+
*/
|
|
29
|
+
export function PluginContextProvider<TBinding extends Binder>({
|
|
30
|
+
value,
|
|
31
|
+
children,
|
|
32
|
+
}: PluginContextProviderProps<TBinding>) {
|
|
33
|
+
return (
|
|
34
|
+
<PluginContextInternal.Provider value={value as PluginContextPartial}>
|
|
35
|
+
{children}
|
|
36
|
+
</PluginContextInternal.Provider>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Options for usePluginContext hook.
|
|
42
|
+
*/
|
|
43
|
+
export interface UsePluginContextOptions {
|
|
44
|
+
/**
|
|
45
|
+
* Set to true when calling from an empty state component.
|
|
46
|
+
* This returns nullable connection fields since no valid connection exists.
|
|
47
|
+
*/
|
|
48
|
+
partial?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Hook to access the plugin context with typed tool caller.
|
|
53
|
+
*
|
|
54
|
+
* @template TBinding - The binding type for typed tool calls
|
|
55
|
+
* @param options - Optional settings
|
|
56
|
+
* @param options.partial - Set to true in empty state components where connection may not exist
|
|
57
|
+
* @throws Error if used outside of PluginContextProvider
|
|
58
|
+
* @throws Error if connection is null but partial option is not set
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```tsx
|
|
62
|
+
* // In route component (connection guaranteed by layout)
|
|
63
|
+
* const { toolCaller, connection } = usePluginContext<typeof REGISTRY_APP_BINDING>();
|
|
64
|
+
* const result = await toolCaller("COLLECTION_REGISTRY_APP_LIST", { limit: 20 });
|
|
65
|
+
*
|
|
66
|
+
* // In empty state component (no connection available)
|
|
67
|
+
* const { session, org } = usePluginContext<typeof REGISTRY_APP_BINDING>({ partial: true });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
export function usePluginContext<TBinding extends Binder = Binder>(options: {
|
|
71
|
+
partial: true;
|
|
72
|
+
}): PluginContextPartial<TBinding>;
|
|
73
|
+
export function usePluginContext<
|
|
74
|
+
TBinding extends Binder = Binder,
|
|
75
|
+
>(): PluginContext<TBinding>;
|
|
76
|
+
export function usePluginContext<TBinding extends Binder = Binder>(
|
|
77
|
+
options?: UsePluginContextOptions,
|
|
78
|
+
): PluginContext<TBinding> | PluginContextPartial<TBinding> {
|
|
79
|
+
const context = useContext(PluginContextInternal);
|
|
80
|
+
if (!context) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"usePluginContext must be used within a PluginContextProvider",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// If partial mode, return as-is with nullable fields
|
|
87
|
+
if (options?.partial) {
|
|
88
|
+
return context as PluginContextPartial<TBinding>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Otherwise, assert that connection exists (routes should always have one)
|
|
92
|
+
if (!context.connectionId || !context.connection || !context.toolCaller) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
"usePluginContext requires a valid connection. Use { partial: true } in empty state components.",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return context as PluginContext<TBinding>;
|
|
99
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topbar Portal
|
|
3
|
+
*
|
|
4
|
+
* A React portal-based system for rendering content into the project topbar
|
|
5
|
+
* from anywhere in the component tree (including plugin routes).
|
|
6
|
+
*
|
|
7
|
+
* Uses createPortal so that portaled content preserves the source tree's
|
|
8
|
+
* React context -- plugin context, query client, etc. all work naturally.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* 1. The app wraps its layout with <TopbarPortalProvider>
|
|
13
|
+
* 2. ProjectTopbar calls useTopbarPortalTargets() and attaches callback refs to slot divs
|
|
14
|
+
* 3. Plugin components render <TopbarPortal side="right">...</TopbarPortal>
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createContext, useContext, useState, type ReactNode } from "react";
|
|
18
|
+
import { createPortal } from "react-dom";
|
|
19
|
+
|
|
20
|
+
export type TopbarSide = "left" | "center" | "right";
|
|
21
|
+
|
|
22
|
+
interface TopbarPortalContextValue {
|
|
23
|
+
/** Current DOM elements for each slot (null until ProjectTopbar mounts) */
|
|
24
|
+
leftEl: HTMLDivElement | null;
|
|
25
|
+
centerEl: HTMLDivElement | null;
|
|
26
|
+
rightEl: HTMLDivElement | null;
|
|
27
|
+
/** Callback refs for ProjectTopbar to register slot elements */
|
|
28
|
+
setLeftEl: (el: HTMLDivElement | null) => void;
|
|
29
|
+
setCenterEl: (el: HTMLDivElement | null) => void;
|
|
30
|
+
setRightEl: (el: HTMLDivElement | null) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const TopbarPortalContext = createContext<TopbarPortalContextValue | null>(
|
|
34
|
+
null,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Provider that manages the portal target DOM elements for the three topbar slots.
|
|
39
|
+
* Place this in the layout tree so it wraps both the topbar and the content area.
|
|
40
|
+
*/
|
|
41
|
+
export function TopbarPortalProvider({ children }: { children: ReactNode }) {
|
|
42
|
+
const [leftEl, setLeftEl] = useState<HTMLDivElement | null>(null);
|
|
43
|
+
const [centerEl, setCenterEl] = useState<HTMLDivElement | null>(null);
|
|
44
|
+
const [rightEl, setRightEl] = useState<HTMLDivElement | null>(null);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<TopbarPortalContext.Provider
|
|
48
|
+
value={{ leftEl, centerEl, rightEl, setLeftEl, setCenterEl, setRightEl }}
|
|
49
|
+
>
|
|
50
|
+
{children}
|
|
51
|
+
</TopbarPortalContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Hook used by the ProjectTopbar component to get callback refs for the portal target divs.
|
|
57
|
+
* Returns stable callback refs that register/unregister the DOM elements in the provider.
|
|
58
|
+
*/
|
|
59
|
+
export function useTopbarPortalTargets() {
|
|
60
|
+
const ctx = useContext(TopbarPortalContext);
|
|
61
|
+
|
|
62
|
+
const leftRef = (el: HTMLDivElement | null) => ctx?.setLeftEl(el);
|
|
63
|
+
const centerRef = (el: HTMLDivElement | null) => ctx?.setCenterEl(el);
|
|
64
|
+
const rightRef = (el: HTMLDivElement | null) => ctx?.setRightEl(el);
|
|
65
|
+
|
|
66
|
+
if (!ctx) return null;
|
|
67
|
+
|
|
68
|
+
return { leftRef, centerRef, rightRef };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Portal component that renders children into one of the topbar slots.
|
|
73
|
+
*
|
|
74
|
+
* Because this uses React createPortal, the children maintain the React context
|
|
75
|
+
* of the component that renders <TopbarPortal> -- not the topbar's context.
|
|
76
|
+
* This means plugin context (connection, toolCaller, etc.) is available.
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* import { TopbarPortal, usePluginContext } from "@decocms/mesh-sdk/plugins";
|
|
81
|
+
*
|
|
82
|
+
* function MyPluginPage() {
|
|
83
|
+
* const { toolCaller } = usePluginContext<typeof MY_BINDING>();
|
|
84
|
+
*
|
|
85
|
+
* return (
|
|
86
|
+
* <>
|
|
87
|
+
* <TopbarPortal side="right">
|
|
88
|
+
* <Button onClick={() => toolCaller("SOME_TOOL", {})}>
|
|
89
|
+
* Action
|
|
90
|
+
* </Button>
|
|
91
|
+
* </TopbarPortal>
|
|
92
|
+
* <div>Page content...</div>
|
|
93
|
+
* </>
|
|
94
|
+
* );
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
export function TopbarPortal({
|
|
99
|
+
side,
|
|
100
|
+
children,
|
|
101
|
+
}: {
|
|
102
|
+
side: TopbarSide;
|
|
103
|
+
children: ReactNode;
|
|
104
|
+
}) {
|
|
105
|
+
const ctx = useContext(TopbarPortalContext);
|
|
106
|
+
if (!ctx) return null;
|
|
107
|
+
|
|
108
|
+
const el =
|
|
109
|
+
side === "left"
|
|
110
|
+
? ctx.leftEl
|
|
111
|
+
: side === "center"
|
|
112
|
+
? ctx.centerEl
|
|
113
|
+
: ctx.rightEl;
|
|
114
|
+
|
|
115
|
+
if (!el) return null;
|
|
116
|
+
|
|
117
|
+
return createPortal(children, el);
|
|
118
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// AI Provider Types — shared between server tool output and client hooks
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export const PROVIDER_IDS = [
|
|
6
|
+
"deco",
|
|
7
|
+
"anthropic",
|
|
8
|
+
"openrouter",
|
|
9
|
+
"google",
|
|
10
|
+
"claude-code",
|
|
11
|
+
] as const;
|
|
12
|
+
|
|
13
|
+
export type ProviderId = (typeof PROVIDER_IDS)[number];
|
|
14
|
+
|
|
15
|
+
/** All known model capability tokens. Sourced from OpenRouter modality strings. */
|
|
16
|
+
export const MODEL_CAPABILITIES = [
|
|
17
|
+
"text",
|
|
18
|
+
"image",
|
|
19
|
+
"vision",
|
|
20
|
+
"audio",
|
|
21
|
+
"video",
|
|
22
|
+
"file",
|
|
23
|
+
"reasoning",
|
|
24
|
+
] as const;
|
|
25
|
+
|
|
26
|
+
export type ModelCapability = (typeof MODEL_CAPABILITIES)[number];
|
|
27
|
+
|
|
28
|
+
export interface AiProviderModelLimits {
|
|
29
|
+
contextWindow: number;
|
|
30
|
+
/** Null means the provider does not advertise a specific cap. */
|
|
31
|
+
maxOutputTokens: number | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AiProviderModelCosts {
|
|
35
|
+
input: number;
|
|
36
|
+
output: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AiProviderModel {
|
|
40
|
+
providerId: ProviderId;
|
|
41
|
+
modelId: string;
|
|
42
|
+
title: string;
|
|
43
|
+
description: string | null;
|
|
44
|
+
logo: string | null;
|
|
45
|
+
capabilities: ModelCapability[];
|
|
46
|
+
limits: AiProviderModelLimits | null;
|
|
47
|
+
costs: AiProviderModelCosts | null;
|
|
48
|
+
/** Client-side only — the credential key ID used to fetch this model. */
|
|
49
|
+
keyId?: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AiProviderKey {
|
|
53
|
+
id: string;
|
|
54
|
+
providerId: ProviderId;
|
|
55
|
+
label: string;
|
|
56
|
+
createdBy: string;
|
|
57
|
+
createdAt: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface AiProviderInfo {
|
|
61
|
+
id: ProviderId;
|
|
62
|
+
name: string;
|
|
63
|
+
description: string;
|
|
64
|
+
logo?: string | null;
|
|
65
|
+
supportedMethods: ("api-key" | "oauth-pkce")[];
|
|
66
|
+
supportsTopUp?: boolean;
|
|
67
|
+
supportsCredits?: boolean;
|
|
68
|
+
}
|
package/src/types/connection.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Uses snake_case field names matching the database schema directly.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
8
9
|
import { z } from "zod";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -23,13 +24,26 @@ const OAuthConfigSchema = z.object({
|
|
|
23
24
|
export type OAuthConfig = z.infer<typeof OAuthConfigSchema>;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
+
* JSON-Schema-safe JSON Schema object.
|
|
28
|
+
*
|
|
29
|
+
* The MCP SDK's ToolSchema uses z.custom() (AssertObjectSchema) for property
|
|
30
|
+
* values inside inputSchema/outputSchema. Zod 4's toJSONSchema() cannot
|
|
31
|
+
* represent z.custom(), throwing "Custom types cannot be represented in JSON
|
|
32
|
+
* Schema". We override only these two fields with a safe equivalent so that
|
|
33
|
+
* all other ToolSchema fields (name, annotations, execution, icons, _meta, …)
|
|
34
|
+
* still flow through automatically when the MCP SDK is updated.
|
|
27
35
|
*/
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
36
|
+
const JsonSchemaObjectSchema = z
|
|
37
|
+
.object({
|
|
38
|
+
type: z.literal("object"),
|
|
39
|
+
properties: z.record(z.string(), z.unknown()).optional(),
|
|
40
|
+
required: z.array(z.string()).optional(),
|
|
41
|
+
})
|
|
42
|
+
.catchall(z.unknown());
|
|
43
|
+
|
|
44
|
+
const ToolDefinitionSchema = ToolSchema.extend({
|
|
45
|
+
inputSchema: JsonSchemaObjectSchema,
|
|
46
|
+
outputSchema: JsonSchemaObjectSchema.optional(),
|
|
33
47
|
});
|
|
34
48
|
|
|
35
49
|
export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;
|
|
@@ -152,20 +166,24 @@ export const ConnectionCreateDataSchema = ConnectionEntitySchema.omit({
|
|
|
152
166
|
tools: true,
|
|
153
167
|
bindings: true,
|
|
154
168
|
status: true,
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
})
|
|
170
|
+
.partial({
|
|
171
|
+
id: true,
|
|
172
|
+
description: true,
|
|
173
|
+
icon: true,
|
|
174
|
+
app_name: true,
|
|
175
|
+
app_id: true,
|
|
176
|
+
connection_url: true,
|
|
177
|
+
connection_token: true,
|
|
178
|
+
connection_headers: true,
|
|
179
|
+
oauth_config: true,
|
|
180
|
+
configuration_state: true,
|
|
181
|
+
configuration_scopes: true,
|
|
182
|
+
metadata: true,
|
|
183
|
+
})
|
|
184
|
+
.extend({
|
|
185
|
+
icon: z.string().nullish(),
|
|
186
|
+
});
|
|
169
187
|
|
|
170
188
|
export type ConnectionCreateData = z.infer<typeof ConnectionCreateDataSchema>;
|
|
171
189
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decopilot SSE Event Types
|
|
3
|
+
*
|
|
4
|
+
* Canonical type definitions for thread statuses and decopilot SSE events.
|
|
5
|
+
* Shared between server (emitter) and client (consumer) for full type safety.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ============================================================================
|
|
9
|
+
// Thread Status
|
|
10
|
+
// ============================================================================
|
|
11
|
+
|
|
12
|
+
/** Persisted thread statuses (written to DB). */
|
|
13
|
+
export const THREAD_STATUSES = [
|
|
14
|
+
"in_progress",
|
|
15
|
+
"requires_action",
|
|
16
|
+
"failed",
|
|
17
|
+
"completed",
|
|
18
|
+
] as const;
|
|
19
|
+
export type ThreadStatus = (typeof THREAD_STATUSES)[number];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Display statuses include "expired" — a virtual status computed at read time
|
|
23
|
+
* for threads stuck in "in_progress" beyond a timeout threshold.
|
|
24
|
+
* Never persisted to DB, but appears in API responses and UI.
|
|
25
|
+
*/
|
|
26
|
+
export const THREAD_DISPLAY_STATUSES = [...THREAD_STATUSES, "expired"] as const;
|
|
27
|
+
export type ThreadDisplayStatus = (typeof THREAD_DISPLAY_STATUSES)[number];
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// SSE Event Type Constants
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
export const DECOPILOT_EVENTS = {
|
|
34
|
+
STEP: "decopilot.step",
|
|
35
|
+
FINISH: "decopilot.finish",
|
|
36
|
+
THREAD_STATUS: "decopilot.thread.status",
|
|
37
|
+
} as const;
|
|
38
|
+
|
|
39
|
+
export type DecopilotEventType =
|
|
40
|
+
(typeof DECOPILOT_EVENTS)[keyof typeof DECOPILOT_EVENTS];
|
|
41
|
+
|
|
42
|
+
export const ALL_DECOPILOT_EVENT_TYPES: DecopilotEventType[] =
|
|
43
|
+
Object.values(DECOPILOT_EVENTS);
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// Event Payloads (discriminated union on `type`)
|
|
47
|
+
// ============================================================================
|
|
48
|
+
|
|
49
|
+
interface BaseDecopilotEvent {
|
|
50
|
+
id: string;
|
|
51
|
+
source: "decopilot";
|
|
52
|
+
/** Thread ID this event relates to */
|
|
53
|
+
subject: string;
|
|
54
|
+
time: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface DecopilotStepEvent extends BaseDecopilotEvent {
|
|
58
|
+
type: typeof DECOPILOT_EVENTS.STEP;
|
|
59
|
+
data: { stepCount: number };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DecopilotFinishEvent extends BaseDecopilotEvent {
|
|
63
|
+
type: typeof DECOPILOT_EVENTS.FINISH;
|
|
64
|
+
data: { status: ThreadStatus };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface DecopilotThreadStatusEvent extends BaseDecopilotEvent {
|
|
68
|
+
type: typeof DECOPILOT_EVENTS.THREAD_STATUS;
|
|
69
|
+
data: { status: ThreadStatus };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type DecopilotSSEEvent =
|
|
73
|
+
| DecopilotStepEvent
|
|
74
|
+
| DecopilotFinishEvent
|
|
75
|
+
| DecopilotThreadStatusEvent;
|
|
76
|
+
|
|
77
|
+
/** Map from event type string → typed payload (useful for generic handlers) */
|
|
78
|
+
export interface DecopilotEventMap {
|
|
79
|
+
[DECOPILOT_EVENTS.STEP]: DecopilotStepEvent;
|
|
80
|
+
[DECOPILOT_EVENTS.FINISH]: DecopilotFinishEvent;
|
|
81
|
+
[DECOPILOT_EVENTS.THREAD_STATUS]: DecopilotThreadStatusEvent;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ============================================================================
|
|
85
|
+
// Server-side Factories (create typed events for SSEHub.emit)
|
|
86
|
+
// ============================================================================
|
|
87
|
+
|
|
88
|
+
export function createDecopilotStepEvent(
|
|
89
|
+
threadId: string,
|
|
90
|
+
stepCount: number,
|
|
91
|
+
): DecopilotStepEvent {
|
|
92
|
+
return {
|
|
93
|
+
id: crypto.randomUUID(),
|
|
94
|
+
type: DECOPILOT_EVENTS.STEP,
|
|
95
|
+
source: "decopilot",
|
|
96
|
+
subject: threadId,
|
|
97
|
+
data: { stepCount },
|
|
98
|
+
time: new Date().toISOString(),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function createDecopilotFinishEvent(
|
|
103
|
+
threadId: string,
|
|
104
|
+
status: ThreadStatus,
|
|
105
|
+
): DecopilotFinishEvent {
|
|
106
|
+
return {
|
|
107
|
+
id: crypto.randomUUID(),
|
|
108
|
+
type: DECOPILOT_EVENTS.FINISH,
|
|
109
|
+
source: "decopilot",
|
|
110
|
+
subject: threadId,
|
|
111
|
+
data: { status },
|
|
112
|
+
time: new Date().toISOString(),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function createDecopilotThreadStatusEvent(
|
|
117
|
+
threadId: string,
|
|
118
|
+
status: ThreadStatus,
|
|
119
|
+
): DecopilotThreadStatusEvent {
|
|
120
|
+
return {
|
|
121
|
+
id: crypto.randomUUID(),
|
|
122
|
+
type: DECOPILOT_EVENTS.THREAD_STATUS,
|
|
123
|
+
source: "decopilot",
|
|
124
|
+
subject: threadId,
|
|
125
|
+
data: { status },
|
|
126
|
+
time: new Date().toISOString(),
|
|
127
|
+
};
|
|
128
|
+
}
|