/** * The `global` facade — aggregated, single-object-param methods over the * engine's app-scope services. Each method maps to one underlying service * call (except `env()`, which fans out and merges); the `Caller` underneath * applies contract validation and hands the call to the transport. Facade * code never sees service tokens, scope routing, or transport details. */ import type { SessionListQuery, SessionSummary, } from '@moonshot-ai/agent-core-v2/app/sessionIndex/sessionIndex'; import type { SessionMeta } from '@moonshot-ai/agent-core-v2/session/sessionMetadata/sessionMetadata'; import type { Page } from '@moonshot-ai/agent-core-v2/persistence/interface/queryStore'; import type { Workspace, WorkspaceUpdate, } from '@moonshot-ai/agent-core-v2/app/workspace/workspace'; import type { ConfigDiagnostic, ConfigInspectValue, ConfigTarget, } from '@moonshot-ai/agent-core-v2/app/config/config'; import type { ProviderConfig } from '@moonshot-ai/agent-core-v2/llm-adapter/provider/provider'; import type { AuthStatus, IOAuthService, OAuthLoginOptions, } from '@moonshot-ai/agent-core-v2/app/auth/auth'; import type { ExperimentalFeatureState } from '@moonshot-ai/agent-core-v2/app/flag/flag'; import type { FsBrowseResponse, FsHomeResponse, } from '@moonshot-ai/agent-core-v2/app/hostFolderBrowser/hostFolderBrowser'; import type { FileMeta } from '@moonshot-ai/agent-core-v2/app/file/fileService'; import type { ModelRecord } from '@moonshot-ai/agent-core-v2/llm-adapter/model/model'; import type { IModelCatalog } from '@moonshot-ai/agent-core-v2/llm-adapter/model/catalog'; import type { IProviderDiscoveryService } from '@moonshot-ai/agent-core-v2/app/kosongConfig/discovery'; import type { McpServerConfig } from '../../contract/mcp.js'; import type { CallOptions } from '../channel.js'; import type { GlobalMcpServerConfig, McpManagedServer, McpServerAuthBeginResult, McpServerAuthStatus, McpServerInspection, McpServerLocator, McpServerTestResult, McpServerTestTarget, } from '@moonshot-ai/agent-core-v2/app/mcpManagement/mcpManagement'; import type { AnonymousProviderInput, GenerateEvent, GenerateInput, GenerateParams, ProviderInput } from './kosong-types.js'; import type { PluginCommandDef, PluginInfo, PluginSummary, PluginUpdateStatus, ReloadSummary, } from '@moonshot-ai/agent-core-v2/app/plugin/types'; import type { CapabilityStatus } from '@moonshot-ai/agent-core-v2/app/capability/types'; /** Low-level caller the klient factory builds: routes + validates one service call. */ export type Caller = ( service: string, method: string, args: unknown[], options?: CallOptions, ) => Promise; /** Scoped variant — the factory's real signature; global methods bind the core scope. */ export type ScopedCaller = ( scope: { readonly workspaceId?: string; readonly sessionId?: string; readonly agentId?: string }, service: string, method: string, args: unknown[], options?: CallOptions, ) => Promise; /** Streaming variant of `ScopedCaller` — returns a validated `AsyncIterable`. */ export type ScopedStreamCaller = ( scope: { readonly workspaceId?: string; readonly sessionId?: string; readonly agentId?: string }, service: string, method: string, args: unknown[], ) => AsyncIterable; // --------------------------------------------------------------------------- // Wire-type aliases for engine-sourced shapes (not direct klient // dependencies) — derived through the service interfaces. // --------------------------------------------------------------------------- export type RefreshProviderModelsResponse = Awaited< ReturnType >; export type OAuthFlowStart = Awaited>; export type OAuthFlowSnapshot = NonNullable>>; export type OAuthLoginCancelResponse = Awaited>; export type OAuthLogoutResponse = Awaited>; export type ModelCatalogItem = Awaited>[number]; export type ProviderCatalogItem = Awaited< ReturnType >[number]; export type SetDefaultModelResponse = Awaited< ReturnType >; export type RefreshProviderModelsOptions = NonNullable< Parameters[0] >; /** String-literal form of the engine's `ConfigTarget` enum, so consumers never import the enum value. */ export type ConfigTargetLiteral = `${ConfigTarget}`; // --------------------------------------------------------------------------- // Facade interfaces // --------------------------------------------------------------------------- export interface GlobalSessionsFacade { list(query: SessionListQuery): Promise>; get(id: string): Promise; countActive(workspaceIds: readonly string[]): Promise; /** * Create a session rooted at `workDir` (the workspace is registered * implicitly), optionally titled. Returns the persisted metadata. No agent * is created — `session(id).agent('main')` materializes it on first use. * `mcpServers` injects ephemeral per-session MCP servers: connected only * for this session, never persisted. */ create(input: { workDir: string; additionalDirs?: readonly string[]; title?: string; mcpServers?: Readonly>; }): Promise; } export interface GlobalWorkspacesFacade { list(): Promise; get(id: string): Promise; createOrTouch(input: { root: string; name?: string }): Promise; update(input: { id: string; patch: WorkspaceUpdate }): Promise; delete(id: string): Promise; } export interface GlobalConfigFacade { get(domain: string): Promise; getAll(): Promise>; inspect(domain: string): Promise>; set(input: { domain: string; patch: unknown; target?: ConfigTargetLiteral }): Promise; replace(input: { domain: string; value: unknown; target?: ConfigTargetLiteral; }): Promise; /** * Replace several domains in ONE atomic write (the engine's * `IConfigService.replaceSections`): a domain mapped to `undefined` is * cleared, domains absent from `sections` are left untouched. */ replaceSections(input: { sections: Record; target?: ConfigTargetLiteral; }): Promise; reload(): Promise; diagnostics(): Promise; } export interface GlobalKosongFacade { // -- Provider --------------------------------------------------------- listProviders(): Promise; getProvider(id: string): Promise; /** Add a named provider (string id + config) or an anonymous single-model provider (object). */ addProvider(id: string, config: ProviderInput): Promise; addProvider(config: AnonymousProviderInput): Promise; removeProvider(id: string): Promise; refreshProviders(opts?: RefreshProviderModelsOptions): Promise; // -- Model ------------------------------------------------------------ listModels(): Promise; setDefaultModel(id: string): Promise; // -- Generate (streaming) ----------------------------------------------- generate( modelId: string, input: GenerateInput, params?: GenerateParams, ): AsyncIterable; } export interface GlobalAuthFacade { status(provider?: string): Promise; summarize(): Promise; /** * The engine's own auth-readiness probe for a model (the default model when * omitted): resolves config-file apiKey / provider env-bag credentials or an * OAuth token, throwing a typed auth error when nothing resolves. Actual * model usage does not depend on the OAuth-only {@link summarize} view. */ ensureReady(modelOverride?: string): Promise; startLogin(provider?: string, options?: OAuthLoginOptions): Promise; flow(provider?: string): Promise; cancelLogin(provider?: string): Promise; logout(provider?: string): Promise; /** * @deprecated Use `kosong.refreshProviders({ scope: 'oauth' })` — the * kosong facade owns provider-model refresh; this alias remains for one * release cycle. */ refreshProviderModels(): Promise; } export interface GlobalFlagsFacade { list(): Promise; enabled(id: string): Promise; enabledIds(): Promise; explain(id: string): Promise; snapshot(): Promise>; } export interface GlobalCapabilitiesFacade { list(): Promise; get(id: string): Promise; install(id: string): Promise; } export interface GlobalPluginsFacade { list(): Promise; info(id: string): Promise; install(source: string): Promise; setEnabled(input: { id: string; enabled: boolean }): Promise; setMcpServerEnabled(input: { id: string; server: string; enabled: boolean }): Promise; remove(id: string): Promise; reload(): Promise; checkUpdates(): Promise; listCommands(): Promise; } export interface GlobalHostFsFacade { browse(absPath?: string): Promise; home(): Promise; } /** * The unified MCP management plane (engine `IMcpManagementService`, App * scope): CRUD on the user-level `mcp.json`, a connection test probe, the * locator-addressed inspection catalog, the auth-status surface, and the * locator-addressed OAuth flow operations. */ export interface GlobalMcpFacade { list(input?: { cwd?: string }): Promise; get(input: { name: string; cwd?: string }): Promise; /** Add a user-level entry; a same-named read-only entry rejects. Returns the refreshed list. */ add(input: { server: GlobalMcpServerConfig; cwd?: string; }): Promise; /** Replace a user-level entry; read-only entries reject. Returns the refreshed list. */ update(input: { server: GlobalMcpServerConfig; cwd?: string; }): Promise; /** Remove a user-level entry; read-only entries reject. Returns the refreshed list. */ remove(input: { name: string; cwd?: string }): Promise; /** Probe a real connection: a registry `name`, or an inline `server` config as-is. */ test(input: McpServerTestTarget): Promise; /** The locator-addressed catalog plus a batched real-connection probe of OAuth candidates. */ inspect(input?: { targets?: readonly McpServerLocator[]; cwd?: string; }): Promise; /** Per-server OAuth state; omitted `verify` detects implicit OAuth, `false` stays offline. */ authStatuses(input?: { cwd?: string; verify?: boolean; }): Promise; /** Resolve a legacy name-only auth target to its unambiguous locator. */ resolveByName(input: { name: string; cwd?: string }): Promise; beginAuth(input: { locator: McpServerLocator; cwd?: string; }): Promise; completeAuth(input: { flowId: string; timeoutMs?: number }): Promise; cancelAuth(input: { flowId: string }): Promise; resetAuth(input: { locator: McpServerLocator; cwd?: string }): Promise; } /** One downloaded upload: its metadata plus the buffered bytes. */ export interface FileDownload { readonly meta: FileMeta; readonly data: Uint8Array; } export interface GlobalFilesFacade { /** * Upload buffered bytes to the daemon's file store. Bytes cross the wire * base64-encoded (JSON cannot carry them), so very large uploads pay one * encode here and one decode in the dispatcher. */ save(input: { data: Uint8Array; filename: string; name?: string; mimeType?: string; expiresInSec?: number; }): Promise; /** Download one upload back into memory. */ get(fileId: string): Promise; delete(fileId: string): Promise; } /** Aggregated host/environment snapshot (`bootstrapService` properties). */ export interface KlientEnvInfo { readonly platform: string; readonly arch: string; readonly cwd: string; readonly osHomeDir: string; readonly homeDir: string; readonly configPath: string; readonly clientVersion: string; readonly sessionsDir: string; readonly blobsDir: string; readonly storeDir: string; readonly cacheDir: string; readonly logsDir: string; } export interface GlobalFacade { readonly sessions: GlobalSessionsFacade; readonly workspaces: GlobalWorkspacesFacade; readonly config: GlobalConfigFacade; readonly kosong: GlobalKosongFacade; readonly auth: GlobalAuthFacade; readonly flags: GlobalFlagsFacade; readonly plugins: GlobalPluginsFacade; readonly capabilities: GlobalCapabilitiesFacade; readonly hostFs: GlobalHostFsFacade; readonly files: GlobalFilesFacade; readonly mcp: GlobalMcpFacade; env(): Promise; } // --------------------------------------------------------------------------- // Implementation — thin reshaping over `Caller`. Casts are safe by // construction: the contract validates outputs, and type-parity assertions // tie every contract schema to its engine type. // --------------------------------------------------------------------------- const ENV_SCALAR_PROPERTIES = [ 'platform', 'arch', 'cwd', 'osHomeDir', 'homeDir', 'configPath', 'sessionsDir', 'blobsDir', 'storeDir', 'cacheDir', 'logsDir', ] as const; // The IPC transport enforces a per-call deadline (default 30s) that would // truncate the completeAuth long poll: the engine waits up to // `DEFAULT_AUTH_TIMEOUT_MS` for the browser callback when the caller omits // `timeoutMs` (agent-core-v2 `mcpManagementService.ts`), and the // authorization-code exchange afterwards is itself bounded at 30s per grant // request (agent-core-v2 `mcpCore/oauth/service.ts`). The per-call deadline // below covers both, so IPC behaves like the timeout-free memory transport. const DEFAULT_AUTH_TIMEOUT_MS = 15 * 60_000; const AUTH_COMPLETION_MARGIN_MS = 30_000; export function createGlobalFacade(scoped: ScopedCaller, scopedStream: ScopedStreamCaller): GlobalFacade { const call: Caller = (service, method, args, options) => scoped({}, service, method, args, options); const streamCall = (service: string, method: string, args: unknown[]) => scopedStream({}, service, method, args); // The bootstrap snapshot is frozen at process start, so the aggregated // env() result can never change — resolve it once and reuse the promise. let envPromise: Promise | undefined; const env = (): Promise => { envPromise ??= Promise.all([ ...ENV_SCALAR_PROPERTIES.map((prop) => call('bootstrapService', prop, []) as Promise), // The wire surface keeps `clientVersion` (a string); it is sourced from // the bootstrap clientIdentity, which replaced the flat scalar. call('bootstrapService', 'clientIdentity', []) as Promise<{ version: string }>, ]).then((values) => { const scalars = Object.fromEntries( ENV_SCALAR_PROPERTIES.map((prop, index) => [prop, values[index]]), ); const identity = values[values.length - 1] as { version: string }; return { ...scalars, clientVersion: identity.version } as unknown as KlientEnvInfo; }); return envPromise; }; return { sessions: { list: (query) => call('sessionIndex', 'listRecent', [query]) as Promise>, get: (id) => call('sessionIndex', 'get', [id]) as Promise, countActive: (workspaceIds) => call('sessionIndex', 'count', [{ workspaceIds }]) as Promise, create: async ({ workDir, additionalDirs, title, mcpServers }) => { const handle = (await scoped({}, 'sessionManager', 'create', [ { workDir, additionalDirs, mcpServers }, ])) as { id: string }; const scope = { sessionId: handle.id }; if (title !== undefined) { await scoped(scope, 'sessionMetadata', 'setTitle', [title]); } return scoped(scope, 'sessionMetadata', 'read', []) as Promise; }, }, workspaces: { list: () => call('workspaceService', 'list', []) as Promise, get: (id) => call('workspaceService', 'get', [id]) as Promise, createOrTouch: ({ root, name }) => call('workspaceService', 'createOrTouch', [root, name]) as Promise, update: ({ id, patch }) => call('workspaceService', 'update', [id, patch]) as Promise, delete: (id) => call('workspaceService', 'delete', [id]) as Promise, }, config: { get: (domain: string) => call('configService', 'get', [domain]) as Promise, getAll: () => call('configService', 'getAll', []) as Promise>, inspect: (domain: string) => call('configService', 'inspect', [domain]) as Promise>, set: ({ domain, patch, target }) => call('configService', 'set', [domain, patch, target]) as Promise, replace: ({ domain, value, target }) => // `null` is the wire encoding of "clear this domain" — JSON // round-trips cannot carry `undefined` (see IConfigService.replace). call('configService', 'replace', [domain, value === undefined ? null : value, target]) as Promise, replaceSections: ({ sections, target }) => call('configService', 'replaceSections', [ Object.fromEntries( Object.entries(sections).map(([domain, value]) => [ domain, value === undefined ? null : value, ]), ), target, ]) as Promise, reload: () => call('configService', 'reload', []) as Promise, diagnostics: () => call('configService', 'diagnostics', []) as Promise, }, kosong: { listProviders: () => call('modelResolver', 'listProviders', []) as Promise< readonly ProviderCatalogItem[] >, getProvider: (id) => call('modelResolver', 'getProvider', [id]) as Promise, addProvider: (( idOrConfig: string | AnonymousProviderInput, maybeConfig?: ProviderInput, ): Promise => { if (typeof idOrConfig === 'string') { // Named provider — map ProviderInput to ProviderConfig wire shape. const config = maybeConfig!; const wire: ProviderConfig = { type: config.type, baseUrl: config.baseUrl, defaultModel: config.defaultModel, apiKey: config.auth.method === 'api-key' ? config.auth.apiKey : '', }; return call('providerService', 'set', [idOrConfig, wire]) as Promise; } // Anonymous provider — map AnonymousProviderInput to ModelRecord wire shape. const anon = idOrConfig; const capabilities = anon.capabilities ? Object.entries(anon.capabilities) .filter(([, v]) => v) .map(([k]) => k) : undefined; const wire: ModelRecord = { model: anon.model, protocol: anon.protocol as ModelRecord['protocol'], baseUrl: anon.baseUrl, apiKey: anon.auth.method === 'api-key' ? anon.auth.apiKey : '', displayName: anon.displayName, maxContextSize: anon.maxContextSize, capabilities, }; return call('modelService', 'set', [anon.id, wire]) as Promise; }) as GlobalKosongFacade['addProvider'], removeProvider: async (id) => { // Try provider registry first; fall back to model registry. const existing = await call('providerService', 'get', [id]); if (existing !== undefined) { return call('providerService', 'delete', [id]) as Promise; } return call('modelService', 'delete', [id]) as Promise; }, refreshProviders: (opts) => call('providerDiscovery', 'refreshProviderModels', [ opts, ]) as Promise, listModels: () => call('modelResolver', 'listModels', []) as Promise, setDefaultModel: (id) => call('modelResolver', 'setDefaultModel', [id]) as Promise, generate: (modelId, input, params) => streamCall('modelResolver', 'generate', [modelId, input, params]) as AsyncIterable, }, auth: { status: (provider) => call('oauthService', 'status', [provider]) as Promise, summarize: () => call('authSummaryService', 'summarize', []) as Promise, ensureReady: (modelOverride) => call('authSummaryService', 'ensureReady', [modelOverride]) as Promise, startLogin: (provider, options) => call('oauthService', 'startLogin', [provider, options]) as Promise, flow: (provider) => call('oauthService', 'getFlow', [provider]) as Promise, cancelLogin: (provider) => call('oauthService', 'cancelLogin', [provider]) as Promise, logout: (provider) => call('oauthService', 'logout', [provider]) as Promise, refreshProviderModels: () => call('oauthService', 'refreshOAuthProviderModels', []) as Promise, }, flags: { list: () => call('flagService', 'explainAll', []) as Promise, enabled: (id) => call('flagService', 'enabled', [id]) as Promise, enabledIds: () => call('flagService', 'enabledIds', []) as Promise, explain: (id) => call('flagService', 'explain', [id]) as Promise, snapshot: () => call('flagService', 'snapshot', []) as Promise>, }, plugins: { list: () => call('pluginService', 'listPlugins', []) as Promise, info: (id) => call('pluginService', 'getPluginInfo', [{ id }]) as Promise, install: (source) => call('pluginService', 'installPlugin', [{ source }]) as Promise, setEnabled: (input) => call('pluginService', 'setPluginEnabled', [input]) as Promise, setMcpServerEnabled: (input) => call('pluginService', 'setPluginMcpServerEnabled', [input]) as Promise, remove: (id) => call('pluginService', 'removePlugin', [{ id }]) as Promise, reload: () => call('pluginService', 'reloadPlugins', []) as Promise, checkUpdates: () => call('pluginService', 'checkUpdates', []) as Promise, listCommands: () => call('pluginService', 'listPluginCommands', []) as Promise, }, capabilities: { list: () => call('capabilityService', 'listCapabilities', []) as Promise, get: (id) => call('capabilityService', 'getCapability', [id]) as Promise, install: (id) => call('capabilityService', 'installCapability', [id]) as Promise, }, hostFs: { browse: (absPath) => call('hostFolderBrowser', 'browse', [absPath]) as Promise, home: () => call('hostFolderBrowser', 'home', []) as Promise, }, files: { save: ({ data, filename, name, mimeType, expiresInSec }) => call('fileService', 'save', [ Buffer.from(data).toString('base64'), filename, { name, mimeType, expiresInSec }, ]) as Promise, get: async (fileId) => { const wire = (await call('fileService', 'get', [fileId])) as { meta: FileMeta; data: string; }; return { meta: wire.meta, data: Buffer.from(wire.data, 'base64') }; }, delete: (fileId) => call('fileService', 'delete', [fileId]) as Promise, }, mcp: { list: (input) => call('mcpManagementService', 'listServers', [ input === undefined ? undefined : { cwd: input.cwd }, ]) as Promise, get: ({ name, cwd }) => call('mcpManagementService', 'getServer', [ name, cwd === undefined ? undefined : { cwd }, ]) as Promise, add: ({ server, cwd }) => call('mcpManagementService', 'addServer', [ server, cwd === undefined ? undefined : { cwd }, ]) as Promise< readonly McpManagedServer[] >, update: ({ server, cwd }) => call('mcpManagementService', 'updateServer', [ server, cwd === undefined ? undefined : { cwd }, ]) as Promise< readonly McpManagedServer[] >, remove: ({ name, cwd }) => call('mcpManagementService', 'removeServer', [ name, cwd === undefined ? undefined : { cwd }, ]) as Promise< readonly McpManagedServer[] >, test: (target) => call('mcpManagementService', 'testServer', [target]) as Promise, inspect: (input) => call('mcpManagementService', 'inspectServers', [ input?.targets, input === undefined ? undefined : { cwd: input.cwd }, ]) as Promise< readonly McpServerInspection[] >, authStatuses: (input) => call('mcpManagementService', 'listAuthStatuses', [ input === undefined ? undefined : { cwd: input.cwd, verify: input.verify }, ]) as Promise, resolveByName: ({ name, cwd }) => call('mcpManagementService', 'resolveServerByName', [name, { cwd }]) as Promise< McpServerLocator >, beginAuth: ({ locator, cwd }) => call('mcpManagementService', 'beginServerAuth', [ locator, { cwd }, ]) as Promise, completeAuth: ({ flowId, timeoutMs }) => call('mcpManagementService', 'completeServerAuth', [{ flowId, timeoutMs }], { // Clamp to Node's 32-bit timer ceiling: `timeoutMs` may legally be // the contract max (2**31 - 1), and adding the margin would // overflow setTimeout into a ~1ms deadline. timeoutMs: Math.min( (timeoutMs ?? DEFAULT_AUTH_TIMEOUT_MS) + AUTH_COMPLETION_MARGIN_MS, 2 ** 31 - 1, ), }) as Promise, cancelAuth: ({ flowId }) => call('mcpManagementService', 'cancelServerAuth', [{ flowId }]) as Promise, resetAuth: ({ locator, cwd }) => call('mcpManagementService', 'resetServerAuth', [locator, { cwd }]) as Promise, }, env, }; }