diff --git a/knip.json b/knip.json index 026215eb4f..b22ef3caaa 100644 --- a/knip.json +++ b/knip.json @@ -415,6 +415,11 @@ "tests/**/*.ts" ] }, + "packages/llm/llm": { + "ignoreDependencies": [ + "zod" + ] + }, "packages/llm/llm-deepseek": { "entry": [ "tests/**/*.spec.ts", diff --git a/packages/host/apiproxy/package.json b/packages/host/apiproxy/package.json index 9942223158..b1eecd9a6b 100644 --- a/packages/host/apiproxy/package.json +++ b/packages/host/apiproxy/package.json @@ -50,16 +50,10 @@ "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-api-session-controller": "workspace:^", "@deepseek-ai/dsh-brand": "workspace:^", - "@deepseek-ai/dsh-commands": "workspace:^", - "@deepseek-ai/dsh-host-directory-picker": "workspace:^", - "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-native-command": "workspace:^", - "@deepseek-ai/dsh-scope": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-session-persistence": "workspace:^", "@deepseek-ai/dsh-session-query": "workspace:^", - "@deepseek-ai/dsh-settings": "workspace:^", - "@deepseek-ai/dsh-skill": "workspace:^", "@deepseek-ai/dsh-util-crypto": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", "fflate": "^0.8.2", @@ -67,14 +61,13 @@ }, "peerDependencies": { "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-agent-presets": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^" }, "devDependencies": { "@deepseek-ai/cordis": "workspace:^", - "@deepseek-ai/dsh-agent-presets": "workspace:^", "@deepseek-ai/dsh-credentials": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-settings": "workspace:^", "@deepseek-ai/dsh-typert-protocol": "workspace:^", "@deepseek-ai/dsh-typert-registry": "workspace:^" } diff --git a/packages/host/apiproxy/src/api-proxy.ts b/packages/host/apiproxy/src/api-proxy.ts index 30844d9bbb..29ebd6b732 100644 --- a/packages/host/apiproxy/src/api-proxy.ts +++ b/packages/host/apiproxy/src/api-proxy.ts @@ -4,19 +4,10 @@ */ import { homedir } from 'node:os' -import { dirname } from 'node:path' import type { Context } from '@deepseek-ai/cordis' import type { ModelSelection } from '@deepseek-ai/dsh-agent' -import type {} from '@deepseek-ai/dsh-agent-presets/types' -import type { SessionId } from '@deepseek-ai/dsh-session' -import { isUserInvocable } from '@deepseek-ai/dsh-skill' -import { - InvalidPresetIdError, PresetExistsError, - PresetNotWritableError, UnknownPresetError, -} from '@deepseek-ai/dsh-agent-presets' -import type { ApiProxy, ConfigurableProviderView } from './api/index.ts' -import { buildModelCatalog } from '@deepseek-ai/dsh-api-session-controller' -import { SessionQueryError } from '@deepseek-ai/dsh-session-query' +import { canOpenNativePath } from '@deepseek-ai/dsh-native-command' +import type { ApiProxy } from './api/index.ts' import { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, @@ -27,77 +18,30 @@ import { type SessionLogCompressionLevel, } from './session-export.ts' import type { SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence' -// Type-only edges: resolve the command-change stream and `ctx.get('skills')`. -import type {} from '@deepseek-ai/dsh-commands' -import type {} from '@deepseek-ai/dsh-skill' -import type { ScopeKey } from '@deepseek-ai/dsh-scope' -import type { RpcError, RpcRequest, RpcResponse } from './api/rpc.ts' -import { canOpenNativePath, openNativePath, openNativeTextFile } from './native-path-opener.ts' - -/** Read live abort state across awaits without treating it as synchronously immutable. */ -function isAborted(signal: AbortSignal): boolean { - return signal.aborted -} +import type { RpcRequest, RpcResponse } from './api/rpc.ts' /** Wrap an ok result echoing the request's rpcId. */ function ok(request: RpcRequest, value: T): RpcResponse { return { rpcId: request.rpcId, result: { ok: true, value } } } -/** Wrap an error result echoing the request's rpcId. */ -function err(request: RpcRequest, error: RpcError): RpcResponse { - return { rpcId: request.rpcId, result: { ok: false, error } } -} - /** Deployment metadata and Host integrations consumed by the API implementation. */ export interface ApiProxyDefaults { /** Current deployment model selection reported by `host.describe`. */ defaultModelSelection: () => ModelSelection /** Project hint reported by `host.describe`; must match Session Controller's default cwd. */ cwd: string - /** Native open-with-default-application; injectable for carrier tests. */ - openPath?: (path: string, signal: AbortSignal) => Promise - /** Native text-editor handoff; injectable for settings-document tests. */ - openTextFile?: (path: string, signal: AbortSignal) => Promise /** Validated DEFLATE level for session-log ZIP entries; defaults to 6. */ sessionExportCompressionLevel?: SessionLogCompressionLevel /** * Whether handing a path to the native opener can work at all — the * `hasDocument` capability the preset roster reports, and the switch * between opening a preset directory and answering its path as text. - * Absent, an injected `openPath` counts as openable and everything else - * falls back to platform detection ({@link canOpenNativePath}). + * Absent, platform detection decides ({@link canOpenNativePath}). */ canOpenPath?: () => boolean } -/** The roster is absent: this deployment composes no agent presets at all. */ -function noRoster(agentPreset: string): RpcError { - return { - code: 'agent-preset-not-found', - message: 'this deployment composes no agent presets', - details: { agentPreset, available: [] }, - } -} - -/** Map one authoring/roster failure onto its wire code. */ -function presetError(agentPreset: string, error: unknown): RpcError { - if (error instanceof UnknownPresetError) { - return { - code: 'agent-preset-not-found', - message: error.message, - details: { agentPreset: error.presetId, available: [...error.available] }, - } - } - if (error instanceof PresetNotWritableError) { - return { code: 'agent-preset-read-only', message: error.message, details: { agentPreset, reason: error.message } } - } - if (error instanceof InvalidPresetIdError || error instanceof PresetExistsError) { - return { code: 'agent-preset-invalid', message: error.message, details: { agentPreset, reason: error.message } } - } - return { code: 'internal', message: `agent preset "${agentPreset}": ${String(error)}`, details: {} } -} - /** * Implement ApiProxy over a composed host context. * @param ctx - a context with the Host spine mounted. @@ -107,75 +51,10 @@ function presetError(agentPreset: string, error: unknown): RpcError { export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiProxy { const sessionExportCompressionLevel = defaults.sessionExportCompressionLevel ?? DEFAULT_SESSION_LOG_COMPRESSION_LEVEL - /** Resolve a Session's live or standing preset scope without resuming it. */ - async function sessionScopeFor( - sessionId: SessionId, - agentPreset: string | undefined, - ): Promise { - const live = ctx.get('agents')?.get(sessionId) - if (live !== undefined) return live - const presets = ctx.get('agentPresets') - if (presets === undefined) return undefined - try { - return await presets.standingKeyFor(agentPreset) - } catch { - // An unknown or unusable recorded preset falls back to the global registry. - return undefined - } - } - - /** Missing-service report shared by the settings domain (skills-domain stance). */ - function settingsAbsent(): RpcError { - return { code: 'internal', message: 'settings service is absent: this deployment does not mount a settings provider (e.g. @deepseek-ai/dsh-settings-file) in its composition', details: {} } - } - - /** Open one Host-resolved target and map native failures onto the wire vocabulary. */ - async function openTarget( - request: RpcRequest, path: string, signal: AbortSignal, - open: (path: string, signal: AbortSignal) => Promise, - ): Promise> { - try { - await open(path, signal) - return ok(request, { opened: true as const }) - } catch (error: unknown) { - if (signal.aborted) { - return err(request, { - code: 'cancelled', - message: 'path open was aborted', - details: {}, - }) - } - return err(request, { - code: 'internal', - message: `path open failed: ${error instanceof Error ? error.message : String(error)}`, - details: {}, - }) - } - } - - /** Open one Host-resolved path with its default application. */ - function openPath( - request: RpcRequest, path: string, signal: AbortSignal, - ): Promise> { - const open = defaults.openPath - ?? ((target: string, openSignal: AbortSignal) => openNativePath(target, openSignal)) - return openTarget(request, path, signal, open) - } - - /** Open one Host-resolved text document in a native editor. */ - function openTextFile( - request: RpcRequest, path: string, signal: AbortSignal, - ): Promise> { - const open = defaults.openTextFile - ?? ((target: string, openSignal: AbortSignal) => openNativeTextFile(target, openSignal)) - return openTarget(request, path, signal, open) - } - /** Whether this deployment can hand a path to a native opener at all. */ function canOpenPaths(): boolean { if (defaults.canOpenPath !== undefined) return defaults.canOpenPath() - // An injected opener is by definition usable; otherwise ask the platform. - return defaults.openPath !== undefined || canOpenNativePath() + return canOpenNativePath() } return { @@ -198,210 +77,6 @@ export function createApiProxy(ctx: Context, defaults: ApiProxyDefaults): ApiPro })) }, - async openPath(request, signal) { - return openPath(request, request.payload.path, signal) - }, - }, - - agentPresets: { - // Only the desktop opener remains here: the roster, selection, and - // authoring calls are the AgentPresets service's own Remote namespace. - async openDocument(request, signal) { - const { agentPreset } = request.payload - const presets = ctx.get('agentPresets') - if (presets === undefined) return err(request, noRoster(agentPreset)) - try { - const preset = await presets.resolve(agentPreset) - // Same line as copy/remove draw: the shipped install is not the - // user's to manage, and pointing an editor into it invites edits an - // upgrade will silently overwrite. - if (preset.trust !== 'user') { - throw new PresetNotWritableError(preset.id, 'it ships with the deployment') - } - // The id resolved against the Host's own roots is what selects the - // directory — no browser payload carries a path in either direction - // unless the deployment has no opener to hand it to. - const directory = dirname(preset.path) - if (!canOpenPaths()) return ok(request, { opened: false as const, path: directory }) - return await openPath(request, directory, signal) - } catch (error: unknown) { - return err(request, presetError(agentPreset, error)) - } - }, - }, - - skills: { - // Skill lookup never creates or resumes an agent: the session address - // resolves to a canonical cwd from the host-resident session header, and - // the view scope is the live agent or the preset's standing key. - async list(request) { - const { sessionId } = request.payload - let cwd: string | undefined - let agentPreset: string | undefined - try { - using observation = await ctx.sessionQuery.observeSession(sessionId) - if (observation.projections === undefined) { - throw new Error('skill catalog requires a projected Session observation') - } - cwd = observation.header.cwd - agentPreset = observation.projections.values.agentPreset ?? undefined - } catch (error: unknown) { - if (error instanceof SessionQueryError - && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') { - return err(request, { - code: 'session-not-found', - message: `session "${sessionId}" not found`, - details: { sessionId }, - }) - } - return err(request, { - code: 'internal', - message: `session "${sessionId}" could not be inspected: ${String(error)}`, - details: {}, - }) - } - if (cwd === undefined) { - // Every served session records its project at create time; a - // cwd-less header is a pre-project legacy log (not served). - return err(request, { code: 'internal', message: `session "${sessionId}" has no project cwd`, details: {} }) - } - // The host registry is layered per scope and serves every session. A - // composition may still realm-mount its own registry instead; that - // instance is invisible to host contexts, so address it through the - // live agent (`agents.get` keeps the no-side-effect stance above). - const live = ctx.agents.get(sessionId) - const presets = ctx.get('agentPresets') - const scoped = live === undefined ? undefined : presets?.serviceFor(live, 'skills') - // A missing service means no composition mounts dsh-skill, not an - // empty catalog. `ctx.get` also - // keeps this handler independent of the gateway plugin's inject list - // (an undeclared `ctx.skills` property read fails the reflect proxy). - const skillRegistry = scoped ?? ctx.get('skills') - if (skillRegistry === undefined) { - return err(request, { code: 'internal', message: 'skill registry is absent: neither this session\'s agent preset nor the host composition mounts @deepseek-ai/dsh-skill', details: {} }) - } - // Resolve the live or recorded preset scope so the catalog matches the - // Session composition without resuming its Agent. - const scope = await sessionScopeFor(sessionId, agentPreset) - try { - const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable) - return ok(request, { - skills: skills.map(skill => ({ - name: skill.name, - description: skill.description, - ...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse }, - modelInvocable: skill.invocation.modelInvocable, - })), - }) - } catch (error: unknown) { - return err(request, { code: 'internal', message: `skill listing failed: ${String(error)}`, details: {} }) - } - }, - }, - - settings: { - async openDocument(request, signal) { - const settings = ctx.get('settings') - if (settings === undefined) return err(request, settingsAbsent()) - if (isAborted(signal)) { - return err(request, { - code: 'cancelled', - message: 'settings document open was aborted', - details: {}, - }) - } - let path: string | undefined - try { - path = await settings.prepareDocument() - } catch (error: unknown) { - if (isAborted(signal)) { - return err(request, { - code: 'cancelled', - message: 'settings document preparation was aborted', - details: {}, - }) - } - return err(request, { - code: 'internal', - message: `settings document preparation failed: ${error instanceof Error ? error.message : String(error)}`, - details: {}, - }) - } - if (path === undefined) { - return err(request, { - code: 'internal', - message: 'settings provider has no local document to open', - details: {}, - }) - } - if (isAborted(signal)) { - return err(request, { - code: 'cancelled', - message: 'settings document open was aborted', - details: {}, - }) - } - return openTextFile(request, path, signal) - }, - }, - - llm: { - providers(request) { - const registered = ctx.llm.listProviders() - const active = new Set(registered.map(provider => provider.id)) - const directory = ctx.llm.listConfigurableProviders() - const declared = new Set(directory.map(entry => entry.provider)) - const views: ConfigurableProviderView[] = directory.map(entry => ({ - provider: entry.provider, - displayName: entry.displayName, - settingsNs: entry.settingsNs, - settingsPath: [...entry.settingsPath], - active: active.has(entry.provider), - ...entry.declared === undefined ? {} : { declared: entry.declared }, - })) - // Routes registered without a directory declaration still appear — - // they exist and serve models — just with no settings address. No - // adapter claimed them, so nothing can say whether they are shipped. - for (const provider of registered) { - if (declared.has(provider.id)) continue - views.push({ - provider: provider.id, - displayName: provider.name, - settingsNs: '', - settingsPath: [], - active: true, - }) - } - return Promise.resolve(ok(request, { providers: views })) - }, - - async models(request) { - return ok(request, await buildModelCatalog(ctx, defaults.defaultModelSelection())) - }, - - async discoverModels(request, signal) { - const { settingsNs, provider, baseURL, api, apiKey } = request.payload - try { - const models = await ctx.llm.discoverModels(settingsNs, { - ...provider === undefined ? {} : { provider }, - ...baseURL === undefined ? {} : { baseURL }, - ...api === undefined ? {} : { api }, - ...apiKey === undefined ? {} : { apiKey }, - ...signal === undefined ? {} : { signal }, - }) - return ok(request, { models }) - } catch (error: unknown) { - // Every failure here is the user's next move, not a transport fault: - // a wrong endpoint, a rejected key, or a protocol with no listing all - // end at the same place — fill the models in by hand. The details - // repeat only what the caller already sent, never the credential. - return err(request, { - code: 'model-discovery-failed', - message: error instanceof Error ? error.message : String(error), - details: { settingsNs, ...baseURL === undefined ? {} : { baseURL } }, - }) - } - }, }, downloads: { diff --git a/packages/host/apiproxy/src/api/agent-presets.schema.ts b/packages/host/apiproxy/src/api/agent-presets.schema.ts deleted file mode 100644 index ad15512ae0..0000000000 --- a/packages/host/apiproxy/src/api/agent-presets.schema.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * agent-presets domain zod schemas (names derived from map keys: - * agentPresetOpenDocumentRequestSchema / agentPresetOpenDocumentValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' - -/** agentPreset.openDocument request payload. */ -export const agentPresetOpenDocumentRequestSchema = z.object({ - agentPreset: z.string().min(1), -}) satisfies z.ZodType>> - -/** agentPreset.openDocument response value. */ -export const agentPresetOpenDocumentValueSchema = z.union([ - z.object({ opened: z.literal(true) }), - z.object({ opened: z.literal(false), path: z.string() }), -]) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/agent-presets.ts b/packages/host/apiproxy/src/api/agent-presets.ts deleted file mode 100644 index a7749ab6aa..0000000000 --- a/packages/host/apiproxy/src/api/agent-presets.ts +++ /dev/null @@ -1,25 +0,0 @@ -/** - * agent-presets domain contract: handing one preset's directory to the - * platform opener, which is the only agent-preset call still carried here. - * - * The roster and its authoring calls are the AgentPresets service's own Remote - * namespace. This one stays because the opener is a Host desktop integration - * rather than a preset operation. - */ - -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** agent-preset-domain unary methods (the map key agentPreset.* of RpcMethodMap). */ -export interface AgentPresetsApi { - /** - * Hand one locally authored preset's DIRECTORY to the platform opener, for - * editing the files, which are the only composition editor. The request - * carries an id, never a path — the Host resolves it — so no browser - * payload can select an arbitrary filesystem target. Where the deployment - * has no native opener (`canOpenPath: false` on `host.describe`), the reply - * carries the resolved directory for the surface to show as text instead. - * Shipped presets are refused: their install is not the user's to manage. - */ - openDocument(request: RpcRequest<{ agentPreset: string }>, signal: AbortSignal): - Promise> -} diff --git a/packages/host/apiproxy/src/api/host.schema.ts b/packages/host/apiproxy/src/api/host.schema.ts index 6735e55639..5429b8cab0 100644 --- a/packages/host/apiproxy/src/api/host.schema.ts +++ b/packages/host/apiproxy/src/api/host.schema.ts @@ -19,13 +19,3 @@ export const hostDescribeValueSchema = z.object({ home: z.string(), canOpenPath: z.boolean(), }) satisfies z.ZodType>> - -/** host.openPath request payload. */ -export const hostOpenPathRequestSchema = z.object({ - path: z.string().min(1), -}) satisfies z.ZodType>> - -/** host.openPath response value. */ -export const hostOpenPathValueSchema = z.object({ - opened: z.literal(true), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/host.ts b/packages/host/apiproxy/src/api/host.ts index ceb7998d2b..b256afbc00 100644 --- a/packages/host/apiproxy/src/api/host.ts +++ b/packages/host/apiproxy/src/api/host.ts @@ -27,14 +27,4 @@ export interface HostApi { canOpenPath: boolean }>> - /** - * Open a filesystem path with the operating system's default application - * (Finder / Explorer / xdg-open hand-off). The browser carrier's - * prefix-wide trust and authentication checks cover this method like every - * other `/api` request. - */ - openPath( - request: RpcRequest<{ path: string }>, - signal: AbortSignal, - ): Promise> } diff --git a/packages/host/apiproxy/src/api/index.ts b/packages/host/apiproxy/src/api/index.ts index 8e33371141..7f6e5e8018 100644 --- a/packages/host/apiproxy/src/api/index.ts +++ b/packages/host/apiproxy/src/api/index.ts @@ -5,19 +5,11 @@ */ import type { HostApi } from './host.ts' -import type { AgentPresetsApi } from './agent-presets.ts' -import type { SkillsApi } from './skills.ts' -import type { SettingsApi } from './settings.ts' -import type { LlmApi } from './llm.ts' import type { DownloadsApi } from './downloads.ts' /** Root interface of the unified API. New client-request domain = one new file pair + one field here + one map row. */ export interface ApiProxy { host: HostApi - skills: SkillsApi - agentPresets: AgentPresetsApi - settings: SettingsApi - llm: LlmApi /** Host-only download surfaces (GET, no wire envelope); absent from IApiClient. */ downloads: DownloadsApi } @@ -28,10 +20,6 @@ export type { ModelReasoningEffort, ModelSelection, } from '@deepseek-ai/dsh-api-session-controller/types' export type { HostApi } from './host.ts' -export type { SkillsApi, SkillEntry } from './skills.ts' -export type { AgentPresetsApi } from './agent-presets.ts' -export type { SettingsApi } from './settings.ts' -export type { ConfigurableProviderView, DiscoveredModelView, LlmApi } from './llm.ts' export type { DownloadsApi } from './downloads.ts' // ---- Message layer: narrow forms (domain-signature view) ---- diff --git a/packages/host/apiproxy/src/api/llm.schema.ts b/packages/host/apiproxy/src/api/llm.schema.ts deleted file mode 100644 index 2c1619c8df..0000000000 --- a/packages/host/apiproxy/src/api/llm.schema.ts +++ /dev/null @@ -1,115 +0,0 @@ -/** - * llm domain zod schemas (names derived from map keys: llmProvidersRequestSchema / - * llmProvidersValueSchema / llmModelsRequestSchema / llmModelsValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' -import type { ConfigurableProviderView, DiscoveredModelView } from './llm.ts' -import type { - ModelCatalogFailure, - ModelCatalogModel, - ModelSelection, - ModelProviderGroup, - ModelReasoning, - ModelReasoningEffort, -} from '@deepseek-ai/dsh-api-session-controller/types' - -/** One adapter-owned reasoning effort. */ -const modelReasoningEffortSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - description: z.string().optional(), -}) satisfies z.ZodType> - -/** Exact-model reasoning metadata. */ -const modelReasoningSchema = z.object({ - efforts: z.array(modelReasoningEffortSchema).min(1), - defaultEffort: z.string().min(1).optional(), -}) satisfies z.ZodType> - -/** One advisory model entry inside a provider group. */ -const modelCatalogModelSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - description: z.string().optional(), - reasoning: modelReasoningSchema.optional(), -}) satisfies z.ZodType> - -/** One successfully loaded provider group. */ -const modelProviderGroupSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - models: z.array(modelCatalogModelSchema), -}) satisfies z.ZodType> - -/** One provider-local catalog failure. */ -const modelCatalogFailureSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1), - message: z.string(), -}) satisfies z.ZodType> - -/** Complete model selection used as the Host default. */ -const modelSelectionSchema = z.object({ - provider: z.string().min(1), - model: z.string().min(1), - reasoningEffort: z.string().min(1).optional(), -}) satisfies z.ZodType> - -/** ConfigurableProviderView row of llm.providers. */ -export const configurableProviderViewSchema = z.object({ - provider: z.string().min(1), - displayName: z.string().min(1), - settingsNs: z.string(), - settingsPath: z.array(z.string()), - active: z.boolean(), - declared: z.boolean().optional(), -}) satisfies z.ZodType> - -/** llm.providers request payload. */ -export const llmProvidersRequestSchema = z.object({}) satisfies z.ZodType>> - -/** llm.providers response value. */ -export const llmProvidersValueSchema = z.object({ - providers: z.array(configurableProviderViewSchema), -}) satisfies z.ZodType>> - -/** llm.models request payload. */ -export const llmModelsRequestSchema = z.object({}) satisfies z.ZodType>> - -/** llm.models response value. */ -export const llmModelsValueSchema = z.object({ - default: modelSelectionSchema, - routableProviders: z.array(z.string().min(1)), - groups: z.array(modelProviderGroupSchema), - failures: z.array(modelCatalogFailureSchema), -}) satisfies z.ZodType>> - -/** DiscoveredModelView row of llm.discoverModels. */ -export const discoveredModelViewSchema = z.object({ - id: z.string().min(1), - name: z.string().min(1).optional(), - contextWindow: z.number().int().positive().optional(), - maxTokens: z.number().int().positive().optional(), -}) satisfies z.ZodType> - -/** llm.discoverModels request payload. */ -export const llmDiscoverModelsRequestSchema = z.object({ - settingsNs: z.string().min(1), - provider: z.string().min(1).optional(), - baseURL: z.string().min(1).optional(), - api: z.string().min(1).optional(), - // Write-only at the host: used for this one interrogation, never stored and - // never returned. It does ride the client's outgoing envelope like every - // other secret-bearing payload (`settings/update`), which - // `subscribeEnvelopes()` observers can see — redacting that tap is a - // configuration-plane-wide change, not this method's to make alone. - apiKey: z.string().min(1).optional(), -}) satisfies z.ZodType>> - -/** llm.discoverModels response value. */ -export const llmDiscoverModelsValueSchema = z.object({ - models: z.array(discoveredModelViewSchema), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/llm.ts b/packages/host/apiproxy/src/api/llm.ts deleted file mode 100644 index 96b5db874e..0000000000 --- a/packages/host/apiproxy/src/api/llm.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * llm domain contract: host-scoped provider topology for configuration - * surfaces. `llm.providers` merges the configurable-provider directory - * (which providers CAN be configured, and where their settings live) with the - * live route registry; `llm.models` is the session-independent model catalog. - * Clients invalidate from the forwarded `llm/adapters-updated` and - * `settings/document-updated` owner events. - */ - -import type { RpcRequest, RpcResponse } from './rpc.ts' -import type { - ModelCatalog, -} from '@deepseek-ai/dsh-api-session-controller/types' - -/** Wire view of one configurable provider. */ -export interface ConfigurableProviderView { - /** Provider route key (`deepseek-official`, `openai`, …). */ - provider: string - /** Human-readable name for configuration surfaces. */ - displayName: string - /** Settings namespace whose section configures this provider. */ - settingsNs: string - /** Path from that section's root to the provider's profile object (empty = whole section). */ - settingsPath: string[] - /** Whether the route is currently registered (its models are requestable). */ - active: boolean - /** - * Whether the owning adapter knows this route only because configuration - * declared it. Absent when the adapter draws no such distinction, so a - * surface must treat absence as "unknown", not as "shipped". - */ - declared?: boolean -} - -/** Llm-domain unary methods (the map keys llm.* of RpcMethodMap). */ -export interface LlmApi { - /** - * List every configurable provider with its live/dormant state, in - * directory declaration order. Routes registered outside the directory - * (an adapter that never declared configurability) are appended with their - * registration identity and no settings address. - */ - providers(request: RpcRequest<{}>): Promise> - - /** - * Host-scoped model catalog over every registered provider route: the - * settings surface's models view, needing no session. Per-provider listing - * failures ride `failures` without failing the sound groups. - */ - models(request: RpcRequest<{}>): Promise> - - /** - * Interrogate a provider endpoint the configuration surface is still - * drafting, and return the models it advertises for the user to adopt. - * - * The payload is the draft, not a stored route: `settingsNs` selects the - * adapter family that answers, and the rest comes from the form. `provider` - * names the route being edited when there is one — an adapter that already - * describes that route answers from its own registry, with better metadata - * and no network call, and needs no endpoint. A route it does not describe is - * asked over the wire, which is what `baseURL`, `api`, and `apiKey` are for. - * - * Nothing is written — the reply is candidates, and only a later - * `settings.mutate` decides what a route serves. `apiKey` is accepted here - * but never stored or returned; a provider whose key is already stored omits - * it and the endpoint answers unauthenticated or refuses. - */ - discoverModels( - request: RpcRequest<{ - settingsNs: string - provider?: string - baseURL?: string - api?: string - apiKey?: string - }>, - signal?: AbortSignal, - ): Promise> -} - -/** Wire view of one model an interrogated endpoint advertises. */ -export interface DiscoveredModelView { - /** Model id the endpoint accepts. */ - id: string - /** Human-readable name when the endpoint supplies one. */ - name?: string - /** Maximum combined request and response context, when disclosed. */ - contextWindow?: number - /** Maximum output tokens, when disclosed. */ - maxTokens?: number -} diff --git a/packages/host/apiproxy/src/api/rpc-map.ts b/packages/host/apiproxy/src/api/rpc-map.ts index 99c8c7e580..e3e67a9c16 100644 --- a/packages/host/apiproxy/src/api/rpc-map.ts +++ b/packages/host/apiproxy/src/api/rpc-map.ts @@ -4,10 +4,6 @@ */ import type { HostApi } from './host.ts' -import type { AgentPresetsApi } from './agent-presets.ts' -import type { SkillsApi } from './skills.ts' -import type { SettingsApi } from './settings.ts' -import type { LlmApi } from './llm.ts' import type { RpcResponse } from './rpc.ts' /** @@ -17,13 +13,6 @@ import type { RpcResponse } from './rpc.ts' */ export interface RpcMethodMap { 'host.describe': HostApi['describe'] - 'host.openPath': HostApi['openPath'] - 'skill.list': SkillsApi['list'] - 'agentPreset.openDocument': AgentPresetsApi['openDocument'] - 'settings.openDocument': SettingsApi['openDocument'] - 'llm.providers': LlmApi['providers'] - 'llm.models': LlmApi['models'] - 'llm.discoverModels': LlmApi['discoverModels'] } /** Business request payload of method K (reaches through the RpcRequest narrow form to payload). */ diff --git a/packages/host/apiproxy/src/api/rpc.schema.ts b/packages/host/apiproxy/src/api/rpc.schema.ts index 434bbb24dc..c8a322fced 100644 --- a/packages/host/apiproxy/src/api/rpc.schema.ts +++ b/packages/host/apiproxy/src/api/rpc.schema.ts @@ -41,7 +41,6 @@ export const rpcErrorSchema: z.ZodType = z.discriminatedUnion('code', z.object({ code: z.literal('agent-preset-not-found'), message: z.string(), details: z.object({ agentPreset: z.string(), available: z.array(z.string()) }) }), z.object({ code: z.literal('agent-preset-invalid'), message: z.string(), details: z.object({ agentPreset: z.string(), reason: z.string() }) }), z.object({ code: z.literal('agent-busy'), message: z.string(), details: z.object({ reason: z.string() }) }), - z.object({ code: z.literal('model-discovery-failed'), message: z.string(), details: z.object({ settingsNs: z.string(), baseURL: z.string().optional() }) }), z.object({ code: z.literal('internal'), message: z.string(), details: z.object({}) }), ]) as unknown as z.ZodType diff --git a/packages/host/apiproxy/src/api/rpc.ts b/packages/host/apiproxy/src/api/rpc.ts index 0981944824..d3d8643301 100644 --- a/packages/host/apiproxy/src/api/rpc.ts +++ b/packages/host/apiproxy/src/api/rpc.ts @@ -36,15 +36,6 @@ export interface RpcErrorDetailsMap { 'agent-preset-not-found': { agentPreset: string; available: readonly string[] } 'agent-preset-invalid': { agentPreset: string; reason: string } 'agent-busy': { reason: string } - /** - * Interrogating a draft provider endpoint did not produce a model listing: - * no adapter family serves the namespace, the protocol has no listing this - * build can read, or the endpoint was unreachable, refused the credential, - * or answered with something else. The message is the adapter's own text — - * it is what the form shows before falling back to hand-entry — and the - * details name the endpoint asked, never the credential offered. - */ - 'model-discovery-failed': { settingsNs: string; baseURL?: string } 'internal': {} } diff --git a/packages/host/apiproxy/src/api/settings.schema.ts b/packages/host/apiproxy/src/api/settings.schema.ts deleted file mode 100644 index fcfcb75cea..0000000000 --- a/packages/host/apiproxy/src/api/settings.schema.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * settings domain zod schemas (names derived from map keys: - * settingsOpenDocumentRequestSchema / settingsOpenDocumentValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' - -/** settings.openDocument request payload. */ -export const settingsOpenDocumentRequestSchema = z.object({}) satisfies z.ZodType>> - -/** settings.openDocument response value. */ -export const settingsOpenDocumentValueSchema = z.object({ - opened: z.literal(true), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/settings.ts b/packages/host/apiproxy/src/api/settings.ts deleted file mode 100644 index 07fc0e72be..0000000000 --- a/packages/host/apiproxy/src/api/settings.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * settings domain contract: what remains of the web face of the user-settings - * seam (`ctx.settings`) once the redacted read and the path-addressed write - * moved to the `settings` Remote namespace. Only the local-document handoff - * stays here, because opening a Host file is a platform action rather than a - * settings read. - */ - -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** Settings-domain unary methods (the map keys settings.* of RpcMethodMap). */ -export interface SettingsApi { - /** - * Materialize the configured local document when absent and ask the Host to - * hand it to the platform text-document opener. macOS forces a text editor; - * Linux and Windows use the desktop file association. The request carries - * no path, so the browser cannot choose an arbitrary Host filesystem target. - */ - openDocument( - request: RpcRequest<{}>, signal: AbortSignal, - ): Promise> -} diff --git a/packages/host/apiproxy/src/api/skills.schema.ts b/packages/host/apiproxy/src/api/skills.schema.ts deleted file mode 100644 index a0a54b6e9f..0000000000 --- a/packages/host/apiproxy/src/api/skills.schema.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * skills domain zod schemas (names derived from map keys: skillListRequestSchema / - * skillListValueSchema). - */ - -import { z } from 'zod' -import type { RequestPayload, ResponseValue } from './rpc-map.ts' -import type { Wire } from './rpc.schema.ts' -import { sessionIdSchema } from './ids.schema.ts' -import type { SkillEntry } from './skills.ts' - -/** SkillEntry row of skill.list. */ -export const skillEntrySchema = z.object({ - name: z.string().min(1), - description: z.string(), - whenToUse: z.string().optional(), - modelInvocable: z.boolean(), -}) satisfies z.ZodType> - -/** skill.list request payload. */ -export const skillListRequestSchema = z.object({ - sessionId: sessionIdSchema, -}) satisfies z.ZodType>> - -/** skill.list response value. */ -export const skillListValueSchema = z.object({ - skills: z.array(skillEntrySchema), -}) satisfies z.ZodType>> diff --git a/packages/host/apiproxy/src/api/skills.ts b/packages/host/apiproxy/src/api/skills.ts deleted file mode 100644 index 61744f05c9..0000000000 --- a/packages/host/apiproxy/src/api/skills.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * skills domain contract: read-only skill catalog lookup addressed by session. - * The session's header cwd resolves to the canonical project root host-side — - * the client never submits a raw path, and skill lookup never creates or - * resumes an Agent. - */ - -import type { SessionId } from '@deepseek-ai/dsh-session/types' -import type { RpcRequest, RpcResponse } from './rpc.ts' - -/** Skill catalog row (wire projection of the host SkillSummary; provider/source vocabulary stays host-side). */ -export interface SkillEntry { - /** Kebab-case identifier the user references as `/name` in the composer. */ - readonly name: string - /** Short routing description. */ - readonly description: string - /** Optional extra routing guidance. */ - readonly whenToUse?: string - /** False marks a user-only skill (`disable-model-invocation`): invocable here, absent from the model catalog. */ - readonly modelInvocable: boolean -} - -/** - * Skill-domain unary methods (the map key skill.* of RpcMethodMap). Listing - * is the domain's only RPC: invocation uses Session Controller's ordinary - * prompt Remote. The host recognizes its leading `/name` token at the pre-step - * boundary (`dsh-tool-skill` injects the rendered body there), so every client - * shares one deterministic path with no dedicated invocation method. - */ -export interface SkillsApi { - /** Lists the user-invocable skill catalog for the session's project. */ - list(request: RpcRequest<{ sessionId: SessionId }>): Promise> -} diff --git a/packages/host/apiproxy/src/fetch/client.ts b/packages/host/apiproxy/src/fetch/client.ts index c33313ece2..4c49240d24 100644 --- a/packages/host/apiproxy/src/fetch/client.ts +++ b/packages/host/apiproxy/src/fetch/client.ts @@ -12,17 +12,7 @@ import type { ClientRequest, RpcMessage, RpcResponse } from '../api/rpc.ts' import { RpcId } from '../api/rpc.ts' import type { Wire } from '../api/rpc.schema.ts' import { serverResponseSchema } from '../api/rpc.schema.ts' -import { - hostDescribeValueSchema, hostOpenPathValueSchema, -} from '../api/host.schema.ts' -import { skillListValueSchema } from '../api/skills.schema.ts' -import { - agentPresetOpenDocumentValueSchema, -} from '../api/agent-presets.schema.ts' -import { - settingsOpenDocumentValueSchema, -} from '../api/settings.schema.ts' -import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSchema } from '../api/llm.schema.ts' +import { hostDescribeValueSchema } from '../api/host.schema.ts' /** * Client consumption face of the contract (shape a): same domain tree as ApiProxy, but unary @@ -39,21 +29,6 @@ import { llmDiscoverModelsValueSchema, llmModelsValueSchema, llmProvidersValueSc export interface IApiClient { host: { describe(payload: RequestPayload<'host.describe'>, signal?: AbortSignal): Promise>> - openPath(payload: RequestPayload<'host.openPath'>, signal?: AbortSignal): Promise>> - } - skills: { - list(payload: RequestPayload<'skill.list'>, signal?: AbortSignal): Promise>> - } - agentPresets: { - openDocument(payload: RequestPayload<'agentPreset.openDocument'>, signal?: AbortSignal): Promise>> - } - settings: { - openDocument(payload: RequestPayload<'settings.openDocument'>, signal?: AbortSignal): Promise>> - } - llm: { - providers(payload: RequestPayload<'llm.providers'>, signal?: AbortSignal): Promise>> - models(payload: RequestPayload<'llm.models'>, signal?: AbortSignal): Promise>> - discoverModels(payload: RequestPayload<'llm.discoverModels'>, signal?: AbortSignal): Promise>> } } @@ -63,13 +38,6 @@ export interface IApiClient { */ const UNARY_VALUE_SCHEMAS: { [K in keyof RpcMethodMap]: z.ZodType>> } = { 'host.describe': hostDescribeValueSchema, - 'host.openPath': hostOpenPathValueSchema, - 'skill.list': skillListValueSchema, - 'agentPreset.openDocument': agentPresetOpenDocumentValueSchema, - 'settings.openDocument': settingsOpenDocumentValueSchema, - 'llm.providers': llmProvidersValueSchema, - 'llm.models': llmModelsValueSchema, - 'llm.discoverModels': llmDiscoverModelsValueSchema, } /** Default timeout for bounded unary calls (rpc-compare 2026-07-19: a hung host must not leave callers pending forever). */ @@ -195,30 +163,6 @@ export abstract class AbstractApiClient implements IApiClient { readonly host: IApiClient['host'] = { describe: (payload, signal) => this.callUnary('host.describe', payload, signal), - openPath: (payload, signal) => this.callUnary('host.openPath', payload, signal), - } - - readonly skills: IApiClient['skills'] = { - list: (payload, signal) => this.callUnary('skill.list', payload, signal), - } - - // Annotated like every sibling, and load-bearing rather than cosmetic: - // inferring this member inlines `AgentPresetEntry` into the emitted - // declaration by the specifier TS picks — the host `index.ts` — which drags - // the whole gateway, and with it the host `Context` merges, into every - // Client program that imports this carrier. - readonly agentPresets: IApiClient['agentPresets'] = { - openDocument: (payload, signal) => this.callUnary('agentPreset.openDocument', payload, signal), - } - - readonly settings: IApiClient['settings'] = { - openDocument: (payload, signal) => this.callUnary('settings.openDocument', payload, signal), - } - - readonly llm: IApiClient['llm'] = { - providers: (payload, signal) => this.callUnary('llm.providers', payload, signal), - models: (payload, signal) => this.callUnary('llm.models', payload, signal), - discoverModels: (payload, signal) => this.callUnary('llm.discoverModels', payload, signal), } } diff --git a/packages/host/apiproxy/src/fetch/handler.ts b/packages/host/apiproxy/src/fetch/handler.ts index d5fb1d0e29..82142ef923 100644 --- a/packages/host/apiproxy/src/fetch/handler.ts +++ b/packages/host/apiproxy/src/fetch/handler.ts @@ -14,17 +14,7 @@ import type { ClientRequest, RpcError, RpcRequest, RpcResponse, ServerResponse } import { RpcId } from '../api/rpc.ts' import type { Wire } from '../api/rpc.schema.ts' import { clientRequestSchema } from '../api/rpc.schema.ts' -import { - hostDescribeRequestSchema, hostOpenPathRequestSchema, -} from '../api/host.schema.ts' -import { skillListRequestSchema } from '../api/skills.schema.ts' -import { - agentPresetOpenDocumentRequestSchema, -} from '../api/agent-presets.schema.ts' -import { - settingsOpenDocumentRequestSchema, -} from '../api/settings.schema.ts' -import { llmDiscoverModelsRequestSchema, llmModelsRequestSchema, llmProvidersRequestSchema } from '../api/llm.schema.ts' +import { hostDescribeRequestSchema } from '../api/host.schema.ts' /** * Unary dispatch table, keyed by (and compiler-locked to) RpcMethodMap: a map row without a @@ -44,13 +34,6 @@ type UnaryRoutes = { const UNARY_ROUTES: UnaryRoutes = { 'host.describe': { schema: hostDescribeRequestSchema, invoke: (api, r) => api.host.describe(r) }, - 'host.openPath': { schema: hostOpenPathRequestSchema, invoke: (api, r, signal) => api.host.openPath(r, signal) }, - 'skill.list': { schema: skillListRequestSchema, invoke: (api, r) => api.skills.list(r) }, - 'agentPreset.openDocument': { schema: agentPresetOpenDocumentRequestSchema, invoke: (api, r, signal) => api.agentPresets.openDocument(r, signal) }, - 'settings.openDocument': { schema: settingsOpenDocumentRequestSchema, invoke: (api, r, signal) => api.settings.openDocument(r, signal) }, - 'llm.providers': { schema: llmProvidersRequestSchema, invoke: (api, r) => api.llm.providers(r) }, - 'llm.models': { schema: llmModelsRequestSchema, invoke: (api, r) => api.llm.models(r) }, - 'llm.discoverModels': { schema: llmDiscoverModelsRequestSchema, invoke: (api, r, signal) => api.llm.discoverModels(r, signal) }, } /** Route lookup that narrows an arbitrary path segment to a map key (single cast point for the string→key refinement). */ diff --git a/packages/host/apiproxy/src/index.ts b/packages/host/apiproxy/src/index.ts index e8816f99ce..474ad6beef 100644 --- a/packages/host/apiproxy/src/index.ts +++ b/packages/host/apiproxy/src/index.ts @@ -14,8 +14,6 @@ import { Context, Service } from '@deepseek-ai/cordis' import z from '@deepseek-ai/schemastery' import type {} from '@deepseek-ai/dsh-agent-default-model' -import type {} from '@deepseek-ai/dsh-api-session-controller' -import type {} from '@deepseek-ai/dsh-host-directory-picker' import type { ApiProxy } from './api/index.ts' import { createApiProxy } from './api-proxy.ts' import { @@ -63,8 +61,7 @@ export interface Config { */ export class ApiProxyService extends Service implements ApiProxy { static inject = [ - 'agentDefaultModel', 'agents', 'attachments', 'directoryPicker', 'llm', 'sessions', 'sessionQuery', - 'sessionController', + 'agentDefaultModel', 'agents', 'attachments', 'sessions', 'sessionQuery', ] static Config: z = z.object({ @@ -74,10 +71,6 @@ export class ApiProxyService extends Service implements ApiProxy { }) readonly host: ApiProxy['host'] - readonly skills: ApiProxy['skills'] - readonly agentPresets: ApiProxy['agentPresets'] - readonly settings: ApiProxy['settings'] - readonly llm: ApiProxy['llm'] readonly downloads: ApiProxy['downloads'] constructor(ctx: Context, config: Config) { @@ -91,10 +84,6 @@ export class ApiProxyService extends Service implements ApiProxy { : { sessionExportCompressionLevel: config.sessionExportCompressionLevel }), }) this.host = api.host - this.skills = api.skills - this.agentPresets = api.agentPresets - this.settings = api.settings - this.llm = api.llm this.downloads = api.downloads } } diff --git a/packages/host/apiproxy/src/native-path-opener.ts b/packages/host/apiproxy/src/native-path-opener.ts deleted file mode 100644 index f8a065c8e2..0000000000 --- a/packages/host/apiproxy/src/native-path-opener.ts +++ /dev/null @@ -1,202 +0,0 @@ -/** - * Cross-platform native path and text-document openers used by the local GUI - * carrier. - * - * The default intent prefers the default browser for documents it renders when - * the platform can name one, then falls back to the default application. WSL - * translates every path for the Windows desktop instead of assuming a Linux - * GUI. The text-editor intent never consults the browser. - */ - -import { release as osRelease } from 'node:os' -import { extname } from 'node:path' -import { runNativeCommand, type NativeCommandRunner } from '@deepseek-ai/dsh-native-command' - -/** Testable command boundary; native implementations never invoke a shell. */ -export type PathOpenerRunner = NativeCommandRunner - -/** Injectable platform facts for deterministic adapter tests. */ -export interface PathOpenerInternals { - platform?: NodeJS.Platform - /** Kernel release override used to distinguish WSL from desktop Linux. */ - osRelease?: string - /** Environment used for WSL markers and the desktop Linux browser convention. */ - env?: NodeJS.ProcessEnv - run?: PathOpenerRunner -} - -/** Documents a browser renders, as opposed to ones an editor merely edits. */ -const BROWSER_DOCUMENTS = new Set(['.html', '.htm', '.xhtml', '.svg']) - -/** - * The macOS bundle registered for `https` — the default browser, as - * LaunchServices records it. The nested version dict is stripped first - * because it carries its own `LSHandlerRoleAll`. - */ -function macBundleForHttps(plist: string): string | undefined { - const stripped = plist.replace(/LSHandlerPreferredVersions\s*=\s*\{[^}]*\};/g, '') - const block = /\{[^{}]*LSHandlerURLScheme\s*=\s*"?https"?;[^{}]*\}/.exec(stripped)?.[0] - if (block === undefined) return undefined - return /LSHandlerRoleAll\s*=\s*"?([\w.-]+)"?;/.exec(block)?.[1] -} - -/** - * Open one browser-renderable document with the default browser. - * @returns true when a browser took it; false when this platform cannot name - * one, or naming it failed — the caller then uses the default application. - */ -async function openInBrowser( - path: string, signal: AbortSignal, platform: NodeJS.Platform, - run: PathOpenerRunner, env: NodeJS.ProcessEnv, -): Promise { - if (platform === 'darwin') { - let bundle: string | undefined - try { - const { stdout } = await run( - 'defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal) - bundle = macBundleForHttps(stdout) - } catch { - // No LaunchServices record (a fresh account never changed a default): - // the content-type handler is then the system's own choice anyway. - return false - } - if (bundle === undefined) return false - await run('open', ['-b', bundle, path], signal) - return true - } - if (platform === 'linux') { - // $BROWSER is the portable convention; desktop-entry resolution through - // xdg-settings needs a launcher this package has no business shipping. - const browser = env.BROWSER - if (browser === undefined || browser === '') return false - await run(browser, [path], signal) - return true - } - // Windows names no browser without reading the UserChoice registry, and its - // .html association is the browser in the ordinary case. - return false -} - -/** Native path-open intent; macOS distinguishes text editing from file association. */ -type PathOpenIntent = 'default' | 'text-editor' - -/** PowerShell single-quoted literal (doubles embedded quotes). */ -function powershellLiteral(path: string): string { - return `'${path.replace(/'/g, "''")}'` -} - -/** Whether one environment marker is set to a non-empty value. */ -function present(value: string | undefined): boolean { - return value !== undefined && value !== '' -} - -/** Distinguish WSL from desktop Linux using its process and kernel markers. */ -function isWsl(internals: PathOpenerInternals): boolean { - const env = internals.env ?? process.env - if (present(env.WSL_DISTRO_NAME) || present(env.WSL_INTEROP)) return true - return (internals.osRelease ?? osRelease()).toLowerCase().includes('microsoft') -} - -/** Open one Windows-resolvable path through its registered desktop application. */ -async function openWindowsPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise { - await run('powershell.exe', [ - '-NoProfile', - '-Command', - `Invoke-Item -LiteralPath ${powershellLiteral(path)}`, - ], signal) -} - -/** Translate a WSL path before handing it to the Windows desktop. */ -async function openWslPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise { - const translated = await run('wslpath', ['-w', path], signal) - signal.throwIfAborted() - const windowsPath = translated.stdout.replace(/[\r\n]+$/, '') - if (windowsPath === '') throw new Error('wslpath returned no Windows path') - await openWindowsPath(windowsPath, signal, run) -} - -/** Dispatch one shell-free platform command for the requested open intent. */ -async function openNativePathWithIntent( - path: string, - signal: AbortSignal, - intent: PathOpenIntent, - internals: PathOpenerInternals = {}, -): Promise { - const platform = internals.platform ?? process.platform - const run = internals.run ?? runNativeCommand - const env = internals.env ?? process.env - const wsl = platform === 'linux' && isWsl(internals) - - if (!wsl && intent === 'default' && BROWSER_DOCUMENTS.has(extname(path).toLowerCase()) - && await openInBrowser(path, signal, platform, run, env)) return - - if (platform === 'darwin') { - await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal) - return - } - - if (platform === 'win32') { - await openWindowsPath(path, signal, run) - return - } - - if (platform === 'linux') { - if (wsl) { - await openWslPath(path, signal, run) - return - } - await run('xdg-open', [path], signal) - return - } - - throw new Error(`native path opener is unsupported on ${platform}`) -} - -/** - * Whether {@link openNativePath} plausibly reaches a desktop on this host. - * - * macOS and Windows always carry a desktop opener; Linux does when it is WSL - * (the Windows desktop takes the path) or a display server is announced. - * A headless or containerised Linux host answers false, which is what lets a - * surface show a path as text instead of offering a button that would spawn - * `xdg-open` into nothing. - * @param internals - platform and environment seam for deterministic tests. - * @returns true when handing a path to the native opener can work at all. - */ -export function canOpenNativePath(internals: PathOpenerInternals = {}): boolean { - const platform = internals.platform ?? process.platform - if (platform === 'darwin' || platform === 'win32') return true - if (platform !== 'linux') return false - const env = internals.env ?? process.env - return isWsl(internals) || present(env.DISPLAY) || present(env.WAYLAND_DISPLAY) -} - -/** - * Open a filesystem path with the operating system's default application, or - * with the default browser when the path names a document a browser renders. - * @param path - absolute or host-resolvable path (caller owns resolution). - * @param signal - caller/connection lifetime; abort terminates the native command. - * @param internals - Platform, environment, and runner hooks for deterministic tests. - */ -export function openNativePath( - path: string, - signal: AbortSignal, - internals: PathOpenerInternals = {}, -): Promise { - return openNativePathWithIntent(path, signal, 'default', internals) -} - -/** - * Open a text document for editing; macOS bypasses the file-type association - * so a YAML association with a browser cannot consume the gesture. - * @param path - absolute or host-resolvable text-document path. - * @param signal - caller/connection lifetime; abort terminates the native command. - * @param internals - Platform and runner hooks for deterministic tests. - */ -export function openNativeTextFile( - path: string, - signal: AbortSignal, - internals: PathOpenerInternals = {}, -): Promise { - return openNativePathWithIntent(path, signal, 'text-editor', internals) -} diff --git a/packages/host/apiproxy/tsconfig.json b/packages/host/apiproxy/tsconfig.json index 1e1d3e37df..048fffcc72 100644 --- a/packages/host/apiproxy/tsconfig.json +++ b/packages/host/apiproxy/tsconfig.json @@ -8,9 +8,6 @@ "src" ], "references": [ - { - "path": "../../settings/settings" - }, { "path": "../../credentials/credentials" }, @@ -29,39 +26,21 @@ { "path": "../../attachment/attachment" }, - { - "path": "../../llm/llm" - }, { "path": "../../core/agent" }, { "path": "../../core/agent-default-model" }, - { - "path": "../../preset/agent-presets" - }, { "path": "../../core/session" }, - { - "path": "../../core/scope" - }, { "path": "../../session/session-persistence" }, { "path": "../../session-query/session-query" }, - { - "path": "../../skill/skill" - }, - { - "path": "../../interaction/commands" - }, - { - "path": "../directory-picker" - }, { "path": "../../runtime-diagnostics/invariants" },