@xopcai/xopc 0.0.20 → 0.0.22
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/dist/extensions/feishu/src/adapters/cli-login.d.ts +8 -0
- package/dist/extensions/feishu/src/adapters/cli-login.js +225 -0
- package/dist/extensions/feishu/src/adapters/cli-login.js.map +1 -0
- package/dist/extensions/feishu/src/adapters/onboard-cli.js +1 -105
- package/dist/extensions/feishu/src/adapters/onboard-cli.js.map +1 -1
- package/dist/extensions/feishu/src/auth/app-registration.d.ts +47 -0
- package/dist/extensions/feishu/src/auth/app-registration.js +122 -0
- package/dist/extensions/feishu/src/auth/app-registration.js.map +1 -0
- package/dist/extensions/feishu/src/plugin.d.ts +2 -0
- package/dist/extensions/feishu/src/plugin.js +2 -0
- package/dist/extensions/feishu/src/plugin.js.map +1 -1
- package/dist/extensions/telegram/src/inbound-processor.js +1 -1
- package/dist/extensions/telegram/src/plugin.d.ts +1 -1
- package/dist/extensions/telegram/src/plugin.js +1 -1
- package/dist/extensions/telegram/src/routing-integration.js +2 -2
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/extensions/weixin/src/plugin.js +1 -1
- package/dist/gateway/static/root/assets/{agents-DbLV2ldC.js → agents-BcLv59-r.js} +2 -2
- package/dist/gateway/static/root/assets/{agents-DbLV2ldC.js.map → agents-BcLv59-r.js.map} +1 -1
- package/dist/gateway/static/root/assets/{apps-page-CDRSbv3l.js → apps-page-Bl-yxbQo.js} +2 -2
- package/dist/gateway/static/root/assets/{apps-page-CDRSbv3l.js.map → apps-page-Bl-yxbQo.js.map} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-BGueHxMv.js +9 -0
- package/dist/gateway/static/root/assets/channels-settings-BGueHxMv.js.map +1 -0
- package/dist/gateway/static/root/assets/{cron-page-D-fhl446.js → cron-page-DsVZzPqv.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-page-D-fhl446.js.map → cron-page-DsVZzPqv.js.map} +1 -1
- package/dist/gateway/static/root/assets/{cron-utils-DqyPqEDr.js → cron-utils-zbRs2yND.js} +2 -2
- package/dist/gateway/static/root/assets/{cron-utils-DqyPqEDr.js.map → cron-utils-zbRs2yND.js.map} +1 -1
- package/dist/gateway/static/root/assets/{dist-BTNDXpKu.js → dist-CDA7gR_M.js} +2 -2
- package/dist/gateway/static/root/assets/{dist-BTNDXpKu.js.map → dist-CDA7gR_M.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-debug-page-CiOtMG3X.js → extension-debug-page-CDLp4DAs.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-debug-page-CiOtMG3X.js.map → extension-debug-page-CDLp4DAs.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-a59AFw7Q.js → extension-page-DwSCjzHO.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-page-a59AFw7Q.js.map → extension-page-DwSCjzHO.js.map} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BQyLvxBY.js → extension-settings-page-Rdmxe24_.js} +2 -2
- package/dist/gateway/static/root/assets/{extension-settings-page-BQyLvxBY.js.map → extension-settings-page-Rdmxe24_.js.map} +1 -1
- package/dist/gateway/static/root/assets/index-D9Wmfh2f.css +1 -0
- package/dist/gateway/static/root/assets/index-DG8WvMbu.js +150 -0
- package/dist/gateway/static/root/assets/index-DG8WvMbu.js.map +1 -0
- package/dist/gateway/static/root/assets/{logs-page-DMSWW0-k.js → logs-page-ChJ0nsPh.js} +2 -2
- package/dist/gateway/static/root/assets/{logs-page-DMSWW0-k.js.map → logs-page-ChJ0nsPh.js.map} +1 -1
- package/dist/gateway/static/root/assets/{sessions-page-CL2E3nPk.js → sessions-page-Cle4fPla.js} +2 -2
- package/dist/gateway/static/root/assets/{sessions-page-CL2E3nPk.js.map → sessions-page-Cle4fPla.js.map} +1 -1
- package/dist/gateway/static/root/assets/settings-page-Dyo2NYdy.js +2 -0
- package/dist/gateway/static/root/assets/settings-page-Dyo2NYdy.js.map +1 -0
- package/dist/gateway/static/root/assets/{skills-page-0rmNu4AL.js → skills-page-B-smhcB2.js} +2 -2
- package/dist/gateway/static/root/assets/{skills-page-0rmNu4AL.js.map → skills-page-B-smhcB2.js.map} +1 -1
- package/dist/gateway/static/root/index.html +2 -2
- package/dist/package.js +1 -1
- package/dist/src/agent/agent-manager.js +6 -6
- package/dist/src/agent/context/workspace-seed.js +1 -1
- package/dist/src/agent/ipc/bus.js +1 -1
- package/dist/src/agent/ipc/inbox.js +1 -1
- package/dist/src/agent/ipc/socket.js +1 -1
- package/dist/src/agent/memory/builtin-memory-store.d.ts +2 -1
- package/dist/src/agent/memory/builtin-memory-store.js +7 -6
- package/dist/src/agent/memory/builtin-memory-store.js.map +1 -1
- package/dist/src/agent/models/manager.js +1 -1
- package/dist/src/agent/prompt/memory/index.d.ts +4 -2
- package/dist/src/agent/prompt/memory/index.js +22 -10
- package/dist/src/agent/prompt/memory/index.js.map +1 -1
- package/dist/src/agent/prompt/service-prompt-builder.js +1 -1
- package/dist/src/agent/service.js +5 -5
- package/dist/src/agent/skills/index.js +1 -1
- package/dist/src/agent/skills/scanner.js +1 -1
- package/dist/src/agent/skills/skill-manage-ops.js +1 -1
- package/dist/src/agent/skills/skill-manager.js +1 -1
- package/dist/src/agent/tools/factory.js +10 -3
- package/dist/src/agent/tools/factory.js.map +1 -1
- package/dist/src/agent/tools/index.d.ts +1 -1
- package/dist/src/agent/tools/memory-tool.d.ts +7 -2
- package/dist/src/agent/tools/memory-tool.js +11 -5
- package/dist/src/agent/tools/memory-tool.js.map +1 -1
- package/dist/src/agent/tools/send-media.js +1 -1
- package/dist/src/agent/tools/skill-manage-tool.js +1 -1
- package/dist/src/agent/tools/write.js +1 -1
- package/dist/src/auth/credentials.js +2 -2
- package/dist/src/auth/sync-provider-auth.js +1 -1
- package/dist/src/channels/attachments/inbound-persist.js +1 -1
- package/dist/src/channels/attachments/outbound-tts-persist.js +1 -1
- package/dist/src/channels/registry.d.ts +1 -1
- package/dist/src/channels/registry.js +25 -1
- package/dist/src/channels/registry.js.map +1 -1
- package/dist/src/chat-commands/builtins/config.js +3 -3
- package/dist/src/chat-commands/builtins/session.js +1 -1
- package/dist/src/chat-commands/context.js +1 -1
- package/dist/src/chat-commands/index.js +1 -1
- package/dist/src/chat-commands/processor.js +1 -1
- package/dist/src/cli/commands/agent.js +1 -1
- package/dist/src/cli/commands/channels.js +20 -2
- package/dist/src/cli/commands/channels.js.map +1 -1
- package/dist/src/cli/commands/doctor/checks/provider-auth.js +1 -1
- package/dist/src/cli/commands/gateway/call.d.ts +2 -0
- package/dist/src/cli/commands/gateway/call.js +90 -0
- package/dist/src/cli/commands/gateway/call.js.map +1 -0
- package/dist/src/cli/commands/gateway/health.d.ts +2 -0
- package/dist/src/cli/commands/gateway/health.js +77 -0
- package/dist/src/cli/commands/gateway/health.js.map +1 -0
- package/dist/src/cli/commands/gateway/index.d.ts +3 -0
- package/dist/src/cli/commands/gateway/index.js +4 -1
- package/dist/src/cli/commands/gateway/probe.d.ts +2 -0
- package/dist/src/cli/commands/gateway/probe.js +102 -0
- package/dist/src/cli/commands/gateway/probe.js.map +1 -0
- package/dist/src/cli/commands/gateway/status.d.ts +0 -3
- package/dist/src/cli/commands/gateway/status.js +107 -24
- package/dist/src/cli/commands/gateway/status.js.map +1 -1
- package/dist/src/cli/commands/gateway.js +7 -1
- package/dist/src/cli/commands/gateway.js.map +1 -1
- package/dist/src/cli/commands/init.js +3 -3
- package/dist/src/cli/commands/update.js +19 -1
- package/dist/src/cli/commands/update.js.map +1 -1
- package/dist/src/cli/utils/gateway-client.d.ts +28 -0
- package/dist/src/cli/utils/gateway-client.js +115 -0
- package/dist/src/cli/utils/gateway-client.js.map +1 -0
- package/dist/src/config/index.js +2 -2
- package/dist/src/config/loader.js +1 -1
- package/dist/src/config/models-json.js +1 -1
- package/dist/src/config/paths-state.d.ts +4 -0
- package/dist/src/config/paths-state.js +9 -1
- package/dist/src/config/paths-state.js.map +1 -1
- package/dist/src/config/profile.js +2 -2
- package/dist/src/config/reload.d.ts +2 -0
- package/dist/src/config/reload.js +9 -1
- package/dist/src/config/reload.js.map +1 -1
- package/dist/src/config/rules.js +12 -2
- package/dist/src/config/rules.js.map +1 -1
- package/dist/src/cron/executor.js +2 -2
- package/dist/src/cron/persistence.js +1 -1
- package/dist/src/cron/run-log-store.js +1 -1
- package/dist/src/extensions/api.d.ts +6 -1
- package/dist/src/extensions/api.js +52 -1
- package/dist/src/extensions/api.js.map +1 -1
- package/dist/src/extensions/health.js +1 -1
- package/dist/src/extensions/loader.d.ts +6 -1
- package/dist/src/extensions/loader.js +21 -2
- package/dist/src/extensions/loader.js.map +1 -1
- package/dist/src/extensions/lockfile.js +1 -1
- package/dist/src/extensions/normalize-manifest.js +33 -0
- package/dist/src/extensions/normalize-manifest.js.map +1 -1
- package/dist/src/extensions/sdk/index.d.ts +1 -1
- package/dist/src/extensions/sdk/index.js.map +1 -1
- package/dist/src/extensions/types/core.d.ts +35 -1
- package/dist/src/extensions/types/manifest.d.ts +14 -0
- package/dist/src/gateway/agents-admin.js +1 -1
- package/dist/src/gateway/hono/lib/config-payload.d.ts +3 -0
- package/dist/src/gateway/hono/lib/config-payload.js +1 -0
- package/dist/src/gateway/hono/lib/config-payload.js.map +1 -1
- package/dist/src/gateway/hono/oauth.js +1 -1
- package/dist/src/gateway/hono/routes/channels.js +111 -0
- package/dist/src/gateway/hono/routes/channels.js.map +1 -1
- package/dist/src/gateway/hono/routes/commands-skills.js +13 -2
- package/dist/src/gateway/hono/routes/commands-skills.js.map +1 -1
- package/dist/src/gateway/hono/routes/config.js +82 -1
- package/dist/src/gateway/hono/routes/config.js.map +1 -1
- package/dist/src/gateway/hono/routes/public-gateway.js +17 -0
- package/dist/src/gateway/hono/routes/public-gateway.js.map +1 -1
- package/dist/src/gateway/hono/routes/sessions.js +16 -0
- package/dist/src/gateway/hono/routes/sessions.js.map +1 -1
- package/dist/src/gateway/hono/routes/status.js +31 -7
- package/dist/src/gateway/hono/routes/status.js.map +1 -1
- package/dist/src/gateway/hono/routes/update.js +118 -15
- package/dist/src/gateway/hono/routes/update.js.map +1 -1
- package/dist/src/gateway/hono/routes/workspace.js +2 -2
- package/dist/src/gateway/hono/sse.js +2 -2
- package/dist/src/gateway/index.js +1 -1
- package/dist/src/gateway/server.js +3 -0
- package/dist/src/gateway/server.js.map +1 -1
- package/dist/src/gateway/service.d.ts +23 -0
- package/dist/src/gateway/service.js +111 -4
- package/dist/src/gateway/service.js.map +1 -1
- package/dist/src/gateway/workspace-heartbeat-path.js +1 -1
- package/dist/src/infra/update-check.js +54 -21
- package/dist/src/infra/update-check.js.map +1 -1
- package/dist/src/infra/update-lock.d.ts +13 -0
- package/dist/src/infra/update-lock.js +67 -0
- package/dist/src/infra/update-lock.js.map +1 -0
- package/dist/src/infra/update-runner.d.ts +6 -5
- package/dist/src/infra/update-runner.js +93 -13
- package/dist/src/infra/update-runner.js.map +1 -1
- package/dist/src/infra/update-startup.js +37 -11
- package/dist/src/infra/update-startup.js.map +1 -1
- package/dist/src/providers/index.js +2 -2
- package/dist/src/providers/model-registry.js +1 -1
- package/dist/src/session/config-store.js +1 -1
- package/dist/src/session/session-title.js +1 -1
- package/dist/src/session/store.js +3 -3
- package/dist/src/utils/logger/audit.js +1 -1
- package/dist/src/utils/logger/log-store.js +1 -1
- package/dist/src/utils/logger/rotation.js +1 -1
- package/dist/src/voice/tts/audio.js +1 -1
- package/package.json +1 -1
- package/dist/gateway/static/root/assets/channels-settings-DyNnMN1-.js +0 -9
- package/dist/gateway/static/root/assets/channels-settings-DyNnMN1-.js.map +0 -1
- package/dist/gateway/static/root/assets/index-BQNdJlkw.css +0 -1
- package/dist/gateway/static/root/assets/index-fGYWcYhm.js +0 -144
- package/dist/gateway/static/root/assets/index-fGYWcYhm.js.map +0 -1
- package/dist/gateway/static/root/assets/settings-page-CSIVMAJE.js +0 -2
- package/dist/gateway/static/root/assets/settings-page-CSIVMAJE.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","names":["writeConfigToDisk"],"sources":["../../../src/gateway/service.ts"],"sourcesContent":["import crypto from 'crypto';\nimport { AgentService } from '../agent/service.js';\nimport { ChannelManager } from '../channels/manager.js';\nimport { CHAT_CHANNEL_ORDER } from '../channels/registry.js';\nimport { MessageBus, MessageBusShutdownError } from '../infra/bus/index.js';\nimport type { Config as SurfaceConfig } from '../config/config-surface.js';\nimport { loadConfig, saveConfig as writeConfigToDisk } from '../config/index.js';\nimport { getWorkspacePath } from '../config/schema.js';\nimport { CronService } from '../cron/index.js';\nimport { computeBundledExtensionExtensionsPatch } from '../extensions/bundled-extension-activation.js';\nimport { ExtensionLoader } from '../extensions/index.js';\nimport { HeartbeatService, heartbeatRunnerConfigFromConfig } from './heartbeat/index.js';\nimport { ConfigHotReloader } from '../config/reload.js';\nimport { SessionManager } from '../session/index.js';\nimport type { Config } from '../config/schema.js';\nimport type { SessionListQuery, ExportFormat } from '../session/types.js';\nimport { resolveGatewayAuth, assertGatewayAuthConfigured, validateToken, extractToken, type ResolvedGatewayAuth } from './auth.js';\nimport { getModelRegistry } from '../providers/index.js';\nimport {\n createLogger,\n getLogDir,\n getLogStats,\n inboundCorrelationMetadataFromAsyncLogContext,\n} from '../utils/logger.js';\nimport {\n resolveConfigPath,\n resolveCronJobsPath,\n resolveStateDir,\n resolveAgentDir,\n resolveWorkspaceExtensionsDir,\n} from '../config/paths.js';\nimport { AgentRunRelay, type RelayEvent } from './agent-run-relay.js';\nimport { ClarifyBridge, type ClarifyBridgeRequest } from './clarify-bridge.js';\nimport { registerClarifyBridge } from './clarify-runtime.js';\nimport {\n deleteManagedSkill as deleteManagedSkillDir,\n installSkillFromZip,\n listManagedSkillDirs,\n} from '../agent/skills/managed-store.js';\nimport {\n downloadSkillZipBuffer,\n fetchMarketplacePackageDetail,\n listSkillPackages,\n resolveSkillZipDownloadUrl,\n resolveSkillsStoreBaseUrl,\n skillIdForMarketplaceInstall,\n type MarketplacePackageDetail,\n type SkillsStoreListParams,\n type SkillsStoreListResponse,\n} from '../agent/skills/skills-store-client.js';\nimport { createSkillConfigManager } from '../agent/skills/config.js';\nimport { removeSkillsLockEntry } from '../agent/skills/hub-lock.js';\nimport type { SkillCatalogEntry } from '../agent/agent-manager.js';\nimport type { ManagedSkillListItem } from '../agent/skills/managed-store.js';\n\nconst log = createLogger('GatewayService');\nimport { PACKAGE_VERSION } from '../package-version.js';\nimport { buildSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { getDefaultAgentId } from '../routing/resolve-route.js';\nimport { prependEnvelopeTimestamp } from '../channels/envelope-timestamp.js';\nimport { scheduleGatewayUpdateCheck } from '../infra/update-startup.js';\nimport { MAX_CHAT_ATTACHMENTS } from './chat-limits.js';\n\n// ========== SSE Event System ==========\n\nexport interface ServiceEvent {\n id: string;\n type: string;\n payload: unknown;\n}\n\ntype EventListener = (event: ServiceEvent) => Promise<void> | void;\n\nconst EVENT_BUFFER_SIZE = 200; // ring buffer per subscriber for Last-Event-ID replay\n\nexport interface GatewayServiceConfig {\n configPath?: string;\n enableHotReload?: boolean;\n}\n\nexport class GatewayService {\n private bus: MessageBus;\n private config: Config;\n private configPath: string;\n private agentService: AgentService;\n private channelManager: ChannelManager;\n private cronService: CronService;\n private extensionLoader: ExtensionLoader | null = null;\n private heartbeatService: HeartbeatService;\n private sessionManager: SessionManager;\n private running = false;\n private configReloader: ConfigHotReloader | null = null;\n private startTime = Date.now();\n private workspacePath: string;\n\n // Authentication\n private auth: ResolvedGatewayAuth;\n\n // SSE event system\n private eventCounter = 0;\n private subscribers = new Map<string, EventListener>();\n private eventBuffers = new Map<string, ServiceEvent[]>();\n\n // Agent run relay for resuming SSE streams\n public readonly runRelay = new AgentRunRelay();\n\n /** Per-run abort for webchat (POST /api/agent/abort or client disconnect). */\n private runAbortControllers = new Map<string, AbortController>();\n\n private stopGatewayUpdateCheck: (() => void) | null = null;\n\n private readonly clarifyBridge = new ClarifyBridge();\n\n /** Maps webchat session key → active `runId` for `clarify` tool routing. */\n private activeWebchatRunBySession = new Map<string, string>();\n\n constructor(private serviceConfig: GatewayServiceConfig = {}) {\n this.bus = new MessageBus();\n this.configPath = serviceConfig.configPath || resolveConfigPath();\n this.config = loadConfig(this.configPath);\n\n // Initialize authentication\n this.auth = resolveGatewayAuth({\n authConfig: this.config.gateway?.auth,\n });\n\n // Validate auth configuration\n assertGatewayAuthConfigured(this.auth);\n\n // Log token info (not the token itself)\n if (this.auth.mode === 'token') {\n const tokenPreview = this.auth.token ? `${this.auth.token.slice(0, 4)}***` : 'none';\n log.info({ mode: this.auth.mode, token: tokenPreview }, 'Authentication configured');\n } else {\n log.info({ mode: this.auth.mode }, 'Authentication disabled');\n }\n\n // Initialize channel manager\n this.channelManager = new ChannelManager(this.config, this.bus);\n\n // Initialize extension loader\n this.workspacePath = getWorkspacePath(this.config) || './workspace';\n this.initializeExtensionLoader();\n\n // Initialize ModelRegistry (loads from models.json)\n const registry = getModelRegistry();\n log.debug({ \n modelCount: registry.getAll().length, \n error: registry.getError() || 'none' \n }, 'ModelRegistry initialized');\n\n // Initialize agent service with extension registry\n const modelConfig = this.config.agents?.defaults?.model;\n const cronRef: { service?: CronService } = {};\n this.agentService = new AgentService(this.bus, {\n workspace: this.workspacePath,\n model: typeof modelConfig === 'string' ? modelConfig : modelConfig?.primary,\n config: this.config,\n extensionRegistry: this.extensionLoader?.getRegistry(),\n getCronService: () => cronRef.service,\n gatewayClarify: {\n requestClarification: (sessionKey, request) => {\n const runId = this.activeWebchatRunBySession.get(sessionKey);\n const publishSse = runId\n ? (e: RelayEvent) => {\n this.agentService.enqueueWebchatSseEvent(sessionKey, e);\n }\n : undefined;\n const parsed = parseSessionKey(sessionKey);\n const deliver =\n parsed?.source === 'telegram'\n ? async (ctx: { sessionKey: string; requestId: string; request: ClarifyBridgeRequest }) => {\n await this.deliverTelegramClarify(ctx);\n }\n : undefined;\n if (!runId && !deliver) {\n return Promise.reject(\n new Error('Clarify is not available for this session (use webchat, Telegram, or CLI)'),\n );\n }\n return this.clarifyBridge.startRequest({\n sessionKey,\n runId,\n relay: this.runRelay,\n publishSse,\n request,\n deliver,\n });\n },\n },\n });\n\n\n // Set channel manager reference for model switching\n this.agentService.setChannelManager(this.channelManager);\n this.channelManager.setSessionModelHooks({\n getModelForSession: (sk) => this.agentService.getModelForSession(sk),\n switchModelForSession: (sk, id) => this.agentService.switchModelForSession(sk, id),\n });\n\n // Initialize cron service\n this.cronService = new CronService({\n filePath: resolveCronJobsPath(),\n agentService: this.agentService,\n messageBus: this.bus,\n });\n cronRef.service = this.cronService;\n\n // Initialize session manager\n this.sessionManager = new SessionManager({\n config: this.config,\n });\n\n this.heartbeatService = new HeartbeatService({\n agentService: this.agentService,\n messageBus: this.bus,\n cronService: this.cronService,\n sessionStore: this.sessionManager.getStore(),\n getConfig: () => this.config,\n });\n\n this.cronService.setDeps({\n agentService: this.agentService,\n messageBus: this.bus,\n heartbeatService: this.heartbeatService,\n getDefaultCronAgentId: () => getDefaultAgentId(this.config),\n });\n }\n\n /**\n * Create extension loader and resolve configs (load runs in start() before channels).\n */\n private initializeExtensionLoader(): void {\n try {\n const aid = getDefaultAgentId(this.config);\n this.extensionLoader = new ExtensionLoader({\n workspaceDir: this.workspacePath,\n extensionsDir: resolveWorkspaceExtensionsDir(this.config, aid),\n });\n this.extensionLoader.setConfig(this.config as Parameters<ExtensionLoader['setConfig']>[0]);\n } catch (error) {\n log.warn({ error }, 'Failed to initialize extension loader');\n }\n }\n\n /**\n * Load extensions and register SDK / full ChannelPlugin instances with ChannelManager.\n */\n private async loadExtensionsAndRegisterChannels(): Promise<void> {\n if (!this.extensionLoader) {\n return;\n }\n try {\n await this.extensionLoader.loadByActivationPlan();\n const reg = this.extensionLoader.getRegistry();\n for (const plugin of reg.channelPlugins) {\n this.channelManager.registerPlugin(plugin);\n }\n log.debug(\n {\n extensionRecords: reg.extensions.size,\n channelPlugins: reg.channelPlugins.length,\n },\n 'Extensions loaded and channel plugins registered',\n );\n } catch (err) {\n log.warn({ err }, 'Failed to load extensions');\n }\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n\n log.debug('Starting gateway service...');\n this.startTime = Date.now();\n this.running = true;\n\n registerClarifyBridge(this.clarifyBridge);\n\n this.channelManager.setOutboundHooks({\n runMessageSending: (to, content, channel) =>\n this.agentService.invokeOutboundMessageSending(to, content, channel),\n runMessageSent: (to, content, success, error, channel) =>\n this.agentService.invokeOutboundMessageSent(to, content, success, error, channel),\n });\n this.channelManager.enableOutboundPersistence(resolveAgentDir(this.config, getDefaultAgentId(this.config)));\n\n if (this.extensionLoader) {\n this.extensionLoader.setRuntimeContext({\n bus: this.bus,\n sessionManager: this.sessionManager,\n });\n }\n\n await this.loadExtensionsAndRegisterChannels();\n\n // Start channels (initialize first, then start)\n await this.channelManager.initialize();\n await this.channelManager.start();\n await this.channelManager.replayPendingOutboundMessages();\n\n // Initialize session manager\n await this.sessionManager.initialize();\n log.debug('Session manager initialized');\n\n this.cronService.setDeps({\n agentService: this.agentService,\n messageBus: this.bus,\n heartbeatService: this.heartbeatService,\n sessionStore: this.sessionManager.getStore(),\n getDefaultCronAgentId: () => getDefaultAgentId(this.config),\n });\n\n this.sessionManager.on('sessionUpdated', (data: { key: string; name?: string; tags?: string[] }) => {\n this.emit('session.updated', { key: data.key, name: data.name, tags: data.tags });\n });\n\n // Start cron service\n if (this.config.cron?.enabled !== false) {\n await this.cronService.initialize();\n }\n\n this.heartbeatService.start(heartbeatRunnerConfigFromConfig(this.config));\n\n // Start agent service (runs in background)\n this.agentService.start().catch((err) => {\n log.error({ err }, 'Agent service error');\n });\n\n // Start outbound message processor\n this.startOutboundProcessor().catch((err) => {\n log.error({ err }, 'Outbound processor error');\n });\n\n // Setup config hot reload\n if (this.serviceConfig.enableHotReload !== false) {\n this.setupConfigReloader();\n }\n\n this.stopGatewayUpdateCheck = scheduleGatewayUpdateCheck({\n config: this.config,\n onUpdateAvailableChange: (update) => {\n this.emit('update.available', update);\n },\n });\n\n log.debug('Gateway service started');\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n\n log.debug('Stopping gateway service...');\n\n if (this.stopGatewayUpdateCheck) {\n this.stopGatewayUpdateCheck();\n this.stopGatewayUpdateCheck = null;\n }\n\n // Stop config reloader\n if (this.configReloader) {\n await this.configReloader.stop();\n this.configReloader = null;\n }\n\n // Stop heartbeat service\n this.heartbeatService.stop();\n\n registerClarifyBridge(null);\n this.clarifyBridge.dispose();\n this.agentService.stop();\n\n // Unblock `consumeOutbound()` / `consumeInbound()` waiters before stopping channels (CLI agent does the same).\n this.running = false;\n this.bus.shutdown();\n\n await this.channelManager.stop();\n\n // Stop cron service\n await this.cronService.stop();\n\n log.debug('Gateway service stopped');\n }\n\n /**\n * Start processing outbound messages and send through channels\n */\n private async startOutboundProcessor(): Promise<void> {\n log.debug('Starting outbound message processor');\n while (this.running) {\n try {\n const msg = await this.bus.consumeOutbound();\n await this.channelManager.send(msg);\n } catch (error) {\n if (error instanceof MessageBusShutdownError) {\n break;\n }\n const em = error instanceof Error ? error.message : String(error);\n log.error(\n { err: error, errorMessage: em, phase: 'outbound_consume' },\n `Outbound pipeline failed (will retry in 1s): ${em}`,\n );\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n }\n }\n\n /**\n * Setup config hot reload using ConfigHotReloader\n */\n private setupConfigReloader(): void {\n this.configReloader = new ConfigHotReloader(\n this.configPath,\n this.config,\n {\n onModelsReload: (newConfig) => this.handleModelsReload(newConfig),\n onAgentDefaultsReload: (newConfig) => this.handleAgentDefaultsReload(newConfig),\n onChannelsReload: (newConfig) => this.handleChannelsReload(newConfig),\n onCronReload: (newConfig) => this.handleCronReload(newConfig),\n onHeartbeatReload: (newConfig) => this.handleHeartbeatReload(newConfig),\n onToolsReload: (newConfig) => this.handleToolsReload(newConfig),\n onFullRestart: (newConfig) => {\n log.warn(\n { requiresProcessRestart: true, hint: 'Restart the gateway process (hot reload cannot apply this change).' },\n 'Config reload: full gateway restart required — see prior \"restartPaths\" info log',\n );\n this.config = newConfig;\n this.emit('config.reload', { section: 'full', requiresRestart: true });\n },\n },\n {\n debounceMs: 300,\n enabled: this.serviceConfig.enableHotReload !== false,\n }\n );\n this.configReloader.start();\n }\n\n /**\n * Handle models config hot reload\n */\n private handleModelsReload(newConfig: Config): void {\n log.debug('Reloading models config...');\n this.config = newConfig;\n getModelRegistry().refresh();\n this.emit('config.reload', { section: 'models' });\n log.debug('Models config reloaded');\n }\n\n /**\n * Handle agent defaults config hot reload\n */\n private handleAgentDefaultsReload(newConfig: Config): void {\n log.debug('Reloading agent defaults...');\n this.config = newConfig;\n this.agentService.applyAgentDefaultsFromConfig(newConfig);\n this.emit('config.reload', { section: 'agents' });\n log.debug('Agent defaults reloaded');\n }\n\n /**\n * Apply `latest.channels` to every registered channel plugin (Telegram, Weixin, extensions).\n * Single runtime path for: file watcher hot reload, API saves, and Weixin QR follow-up.\n */\n private async handleChannelsReload(newConfig: Config): Promise<void> {\n log.debug('Reloading channels config...');\n this.config = newConfig;\n await this.channelManager.updateConfig(newConfig);\n this.emit('config.reload', { section: 'channels' });\n this.emit('channels.status', { channels: this.getChannelsStatus() });\n log.debug('Channels config reloaded');\n }\n\n /** After persisting config to disk: align plugins + debounced reload baseline (watchers skip duplicate diffs). */\n private async syncChannelPluginsAfterPersist(): Promise<void> {\n await this.handleChannelsReload(this.config);\n this.configReloader?.syncCurrentConfig(this.config);\n }\n\n /**\n * Handle cron config hot reload\n */\n private handleCronReload(newConfig: Config): void {\n log.debug('Reloading cron config...');\n this.config = newConfig;\n this.cronService.updateConfig(newConfig);\n this.emit('config.reload', { section: 'cron' });\n log.debug('Cron config reloaded');\n }\n\n /**\n * Handle heartbeat config hot reload\n */\n private handleHeartbeatReload(newConfig: Config): void {\n log.debug('Reloading heartbeat config...');\n this.config = newConfig;\n this.heartbeatService.updateConfig(newConfig);\n this.emit('config.reload', { section: 'heartbeat' });\n log.debug('Heartbeat config reloaded');\n }\n\n /**\n * Apply `gateway.heartbeat` from current config after PATCH /api/config (and when hot reload is off).\n * File watcher uses `handleHeartbeatReload` with the same effect when paths match.\n */\n reloadHeartbeatFromCurrentConfig(): void {\n this.handleHeartbeatReload(this.config);\n }\n\n /**\n * Handle tools config hot reload\n */\n private handleToolsReload(newConfig: Config): void {\n log.debug('Reloading tools config...');\n this.config = newConfig;\n this.emit('config.reload', { section: 'tools' });\n log.debug('Tools config reloaded');\n }\n\n /**\n * Reload configuration from disk (manual trigger)\n */\n async reloadConfig(): Promise<{ reloaded: boolean; error?: string }> {\n if (!this.configReloader) {\n return { reloaded: false, error: 'Config reloader not initialized' };\n }\n const result = await this.configReloader.triggerReload();\n return { reloaded: result.success, error: result.error };\n }\n\n /**\n * After Weixin QR login: token files may change without a `channels.weixin` JSON diff, so run the same\n * channel apply as an API save, then force Weixin long-poll restart (see `reloadMonitorsWithConfig`).\n */\n async afterWeixinCredentialsPersisted(): Promise<void> {\n const next = loadConfig(this.configPath);\n this.config = next;\n this.agentService.applyAgentDefaultsFromConfig(next);\n this.configReloader?.syncCurrentConfig(next);\n await this.handleChannelsReload(next);\n const { weixinPlugin } = await import('../channels/weixin/index.js');\n await weixinPlugin.reloadMonitorsWithConfig(this.config, this.bus);\n log.info('Weixin monitors restarted after credential login');\n }\n\n /**\n * Save current config to disk\n */\n /**\n * Persist and replace `this.config` with the validated file contents so runtime matches disk\n * (PATCH merge objects can drift from Zod-normalized output).\n */\n private async writeConfigAndReloadFromDisk(configToWrite: Config): Promise<void> {\n await writeConfigToDisk(configToWrite, this.configPath);\n this.config = loadConfig(this.configPath);\n this.agentService.applyAgentDefaultsFromConfig(this.config);\n }\n\n async saveConfig(config: Config): Promise<{ saved: boolean; error?: string }> {\n try {\n await this.writeConfigAndReloadFromDisk(config);\n await this.syncChannelPluginsAfterPersist();\n return { saved: true };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n log.error({ error }, 'Failed to save config');\n return { saved: false, error };\n }\n }\n\n /**\n * App store (phase 1): persist `extensions.enabled` / `extensions.disabled` for a bundled extension.\n * Extension modules are loaded at gateway startup; restart the gateway process to fully apply load/unload.\n */\n async setBundledExtensionActivationTarget(\n extensionId: string,\n wanted: boolean,\n ): Promise<{ ok: boolean; error?: string; requiresGatewayRestart: boolean }> {\n const loader = this.extensionLoader;\n if (!loader) {\n return { ok: false, error: 'Extension loader unavailable', requiresGatewayRestart: false };\n }\n const id = extensionId.trim();\n if (!id) {\n return { ok: false, error: 'Invalid extension id', requiresGatewayRestart: false };\n }\n const patch = computeBundledExtensionExtensionsPatch(loader, this.config, id, wanted);\n if (patch.ok === false) {\n return { ok: false, error: patch.error, requiresGatewayRestart: false };\n }\n const newConfig = { ...this.config, extensions: patch.extensions } as Config;\n const saved = await this.saveConfig(newConfig);\n if (!saved.saved) {\n return { ok: false, error: saved.error ?? 'Failed to save config', requiresGatewayRestart: false };\n }\n loader.setConfig(this.config as unknown as SurfaceConfig);\n return { ok: true, requiresGatewayRestart: true };\n }\n\n /**\n * Update configuration and persist to disk\n */\n async updateConfig(updates: Partial<Config>): Promise<{ updated: boolean; error?: string }> {\n try {\n log.debug('Updating configuration...');\n \n // Merge updates\n this.config = { ...this.config, ...updates };\n\n await this.writeConfigAndReloadFromDisk(this.config);\n await this.syncChannelPluginsAfterPersist();\n\n log.debug('Configuration updated successfully');\n return { updated: true };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n log.error({ error }, 'Failed to update config');\n return { updated: false, error };\n }\n }\n\n /**\n * Run agent with a message and stream events\n */\n /**\n * @param runOptions.signal — When set (e.g. client disconnect), aborts in-flight generation and persists partial output.\n */\n async *runAgent(\n message: string,\n channel: string,\n chatId: string,\n attachments?: Array<{\n type: string;\n mimeType?: string;\n data?: string;\n name?: string;\n size?: number;\n }>,\n thinking?: string,\n runOptions?: { signal?: AbortSignal },\n ): AsyncGenerator<{ type: string; content?: string; status?: string; runId?: string }, { status: string; summary: string }, unknown> {\n const cappedAttachments =\n attachments && attachments.length > MAX_CHAT_ATTACHMENTS\n ? attachments.slice(0, MAX_CHAT_ATTACHMENTS)\n : attachments;\n if (attachments && cappedAttachments && attachments.length > cappedAttachments.length) {\n log.debug(\n { dropped: attachments.length - cappedAttachments.length, max: MAX_CHAT_ATTACHMENTS },\n 'Attachments capped for webchat',\n );\n }\n\n const runId = crypto.randomUUID();\n\n // For webchat, register the run in the relay before yielding the first event\n if (channel === 'webchat') {\n const parsedKey = parseSessionKey(chatId);\n const sessionKey = parsedKey ? chatId : buildSessionKey({\n agentId: getDefaultAgentId(this.config),\n source: 'webchat',\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n this.runRelay.ensureRun(runId, sessionKey);\n this.runAbortControllers.set(runId, new AbortController());\n }\n\n const statusEvent = { type: 'status', status: 'accepted', runId };\n if (channel === 'webchat') this.runRelay.publish(runId, statusEvent);\n yield statusEvent;\n\n try {\n // For 'webchat' channel (web UI), process through agent service\n if (channel === 'webchat') {\n // Determine session key: if chatId is already a valid session key, use it directly\n // Otherwise, build a new session key from the chatId\n const parsedKey = parseSessionKey(chatId);\n const sessionKey = parsedKey ? chatId : buildSessionKey({\n agentId: getDefaultAgentId(this.config),\n source: 'webchat',\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n\n const timezone = this.agentService.resolveUserTimezoneForSession(sessionKey);\n // Keep UI clean: persist raw user text (no envelope timestamp),\n // but include a stamped variant for the model so it has a stable \"now\".\n const stampedMessage = prependEnvelopeTimestamp(message, timezone);\n const prepared = await this.agentService.prepareInboundAttachments(sessionKey, cappedAttachments);\n\n // Persist before streaming so a mid-turn refresh still sees text + attachment refs on disk.\n try {\n await this._saveUserMessage(sessionKey, message, prepared);\n } catch (err) {\n log.error({ err, sessionKey }, 'Failed to save user message');\n }\n\n const runAbort = this.runAbortControllers.get(runId);\n if (!runAbort) {\n throw new Error('run abort controller missing for webchat');\n }\n const mergedSignal = runOptions?.signal\n ? AbortSignal.any([runOptions.signal, runAbort.signal])\n : runAbort.signal;\n\n this.activeWebchatRunBySession.set(sessionKey, runId);\n try {\n this.emit('agent.stream', { sessionKey, event: statusEvent });\n const eventStream = this.agentService.processDirectStreaming(stampedMessage, sessionKey, prepared, thinking, {\n signal: mergedSignal,\n });\n\n for await (const event of eventStream) {\n this.runRelay.publish(runId, event);\n this.emit('agent.stream', { sessionKey, event });\n yield event as { type: string; content?: string; status?: string; runId?: string };\n }\n\n this.runRelay.complete(runId);\n try {\n const metaAfter = await this.sessionManager.getSessionMetadata(sessionKey);\n if (metaAfter?.name) {\n this.emit('session.updated', { key: sessionKey, name: metaAfter.name });\n }\n } catch {\n /* ignore */\n }\n return {\n status: mergedSignal.aborted ? 'aborted' : 'ok',\n summary: mergedSignal.aborted ? 'Interrupted' : 'Message processed successfully',\n };\n } catch (error) {\n log.error({ error }, 'Agent processing failed');\n const errorEvent = { type: 'error', content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` };\n this.runRelay.publish(runId, errorEvent);\n this.emit('agent.stream', { sessionKey, event: errorEvent });\n this.runRelay.complete(runId);\n yield errorEvent;\n return { status: 'error', summary: error instanceof Error ? error.message : 'Unknown error' };\n } finally {\n this.activeWebchatRunBySession.delete(sessionKey);\n this.runAbortControllers.delete(runId);\n }\n }\n\n // Send message through bus for other channels (telegram, etc.)\n const correlationMeta = inboundCorrelationMetadataFromAsyncLogContext();\n await this.bus.publishInbound({\n channel,\n sender_id: 'gateway',\n chat_id: chatId,\n content: message,\n ...(correlationMeta ? { metadata: correlationMeta } : {}),\n });\n\n // Wait for and collect response\n // This is simplified - in production we'd need proper session tracking\n yield { type: 'token', content: 'Processing...\\n' };\n\n // Simulate processing delay\n await new Promise((resolve) => setTimeout(resolve, 1000));\n\n yield { type: 'token', content: 'Done\\n' };\n\n return { status: 'ok', summary: 'Message processed' };\n } catch (error) {\n log.error({ error }, 'Agent run failed');\n throw error;\n }\n }\n\n /** Abort an in-flight webchat agent run (matches `runId` from SSE `status`). */\n abortAgentRun(runId: string): boolean {\n this.clarifyBridge.cancelForRun(runId);\n for (const [sk, id] of this.activeWebchatRunBySession) {\n if (id === runId) {\n this.activeWebchatRunBySession.delete(sk);\n }\n }\n const c = this.runAbortControllers.get(runId);\n if (!c) {\n return false;\n }\n c.abort();\n return true;\n }\n\n /**\n * Queue steering text for an active webchat run (`Agent.steer` / tool-boundary injection).\n * `chatId` is the same as `POST /api/agent` body (`sessionKey` or legacy peer id).\n */\n async steerWebchatAgent(\n chatId: string,\n message: string,\n ): Promise<{ ok: true } | { ok: false; code: 'BAD_REQUEST' | 'NO_ACTIVE_RUN' | 'STEER_FAILED' }> {\n const trimmed = message.trim();\n if (!trimmed) {\n return { ok: false, code: 'BAD_REQUEST' };\n }\n const parsedKey = parseSessionKey(chatId);\n const sessionKey = parsedKey\n ? chatId\n : buildSessionKey({\n agentId: getDefaultAgentId(this.config),\n source: 'webchat',\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n if (!this.activeWebchatRunBySession.has(sessionKey)) {\n return { ok: false, code: 'NO_ACTIVE_RUN' };\n }\n const steered = await this.agentService.steerWebchatSession(sessionKey, trimmed);\n if (!steered) {\n return { ok: false, code: 'STEER_FAILED' };\n }\n return { ok: true };\n }\n\n private async deliverTelegramClarify(ctx: {\n sessionKey: string;\n requestId: string;\n request: ClarifyBridgeRequest;\n }): Promise<void> {\n const parsed = parseSessionKey(ctx.sessionKey);\n if (!parsed || parsed.source !== 'telegram') {\n return;\n }\n\n let body = ctx.request.question;\n if (ctx.request.default) {\n body += `\\n\\nDefault if unsure: ${ctx.request.default}`;\n }\n\n const choices = ctx.request.choices;\n const buttonRows =\n choices && choices.length >= 2\n ? choices.map((c, i) => [\n {\n text: c.length > 64 ? `${c.slice(0, 61)}…` : c,\n callback_data: `clarify:${ctx.requestId}:${i}`,\n },\n ])\n : undefined;\n\n if (!buttonRows) {\n body += '\\n\\nReply with your answer in this chat.';\n }\n\n await this.channelManager.send({\n channel: 'telegram',\n chat_id: parsed.peerId,\n content: body,\n metadata: {\n accountId: parsed.accountId,\n ...(parsed.threadId ? { threadId: parsed.threadId } : {}),\n },\n buttons: buttonRows,\n });\n }\n\n /** Deliver a user's answer to a pending `clarify` tool call. */\n submitClarifyResponse(requestId: string, answer: string): boolean {\n return this.clarifyBridge.handleResponse(requestId, answer);\n }\n\n /**\n * Send message through a channel\n */\n async sendMessage(\n channel: string,\n chatId: string,\n content: string\n ): Promise<{ sent: boolean; messageId?: string }> {\n try {\n await this.channelManager.send({\n channel,\n chat_id: chatId,\n content,\n });\n const messageId = `msg_${Date.now()}`;\n this.emit('message.sent', { channel, chatId, messageId });\n return { sent: true, messageId };\n } catch (error) {\n log.error({ channel, chatId, error }, 'Failed to send message');\n throw error;\n }\n }\n\n /**\n * Get channel statuses\n */\n getChannelsStatus(): Array<{\n name: string;\n enabled: boolean;\n connected: boolean;\n }> {\n const runningChannels = new Set(this.channelManager.getRunningChannels());\n const channels = this.config.channels as Record<string, { enabled?: boolean } | undefined> | undefined;\n const builtinOrder = CHAT_CHANNEL_ORDER as readonly string[];\n\n const rows: Array<{ name: string; enabled: boolean; connected: boolean }> = CHAT_CHANNEL_ORDER.map(\n (name) => ({\n name,\n enabled: !!channels?.[name]?.enabled,\n connected: runningChannels.has(name),\n }),\n );\n\n const extReg = this.extensionLoader?.getRegistry();\n const extraIds = extReg?.channelPlugins.map((p) => p.id).filter((id) => !builtinOrder.includes(id)) ?? [];\n if (extraIds.length === 0) {\n return rows;\n }\n\n const seen = new Set(builtinOrder);\n for (const name of extraIds) {\n if (seen.has(name)) continue;\n seen.add(name);\n rows.push({\n name,\n enabled: channels?.[name]?.enabled !== false,\n connected: runningChannels.has(name),\n });\n }\n\n return rows;\n }\n\n /**\n * Request an immediate heartbeat run (coalesced like interval/cron wakes).\n */\n requestHeartbeatNow(opts?: { reason?: string }): void {\n this.heartbeatService.requestNow({ reason: opts?.reason ?? 'manual' });\n }\n\n /**\n * Get health status\n */\n getHealth(): {\n status: string;\n service: string;\n version: string;\n uptime: number;\n channels: { running: number; total: number };\n configPath: string;\n logs?: {\n dir: string;\n errors24h: number;\n stats: Record<string, number>;\n };\n } {\n const runningChannels = this.channelManager.getRunningChannels();\n const allChannels = this.channelManager.getAllChannels();\n const logStats = getLogStats();\n\n return {\n status: 'ok',\n service: 'xopc-gateway',\n version: PACKAGE_VERSION,\n uptime: Math.floor((Date.now() - this.startTime) / 1000),\n channels: {\n running: runningChannels.length,\n total: allChannels.length,\n },\n configPath: this.configPath,\n logs: {\n dir: getLogDir(),\n errors24h: logStats.errorsLast24h,\n stats: logStats.byLevel,\n },\n };\n }\n\n get isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Get extension registry for external access (HTTP routes, gateway methods)\n */\n getExtensionRegistry() {\n return this.extensionLoader?.getRegistry();\n }\n\n /** Extension loader for discovery and frontend asset APIs (may be null if extensions failed to init). */\n getExtensionLoader(): ExtensionLoader | null {\n return this.extensionLoader;\n }\n\n /**\n * Get model registry for external access (HTTP routes)\n */\n getModelRegistry() {\n const { getModelRegistry } = require('../providers/index.js');\n return getModelRegistry();\n }\n\n /**\n * Invoke a gateway method registered by extensions\n */\n async invokeGatewayMethod(method: string, params: Record<string, unknown>): Promise<unknown> {\n const registry = this.getExtensionRegistry();\n if (!registry) {\n throw new Error('Extension registry not available');\n }\n\n const handler = registry.getGatewayMethod(method);\n if (!handler) {\n throw new Error(`Gateway method not found: ${method}`);\n }\n\n return await handler(params);\n }\n\n get currentConfig(): Config {\n return this.config;\n }\n\n get cronServiceInstance(): CronService {\n return this.cronService;\n }\n\n getSkillsApi(): { catalog: SkillCatalogEntry[]; managed: ManagedSkillListItem[] } {\n return {\n catalog: this.agentService.getSkillCatalog(),\n managed: listManagedSkillDirs(),\n };\n }\n\n getSkillMarkdownSource(skillName: string): { name: string; markdown: string } | null {\n return this.agentService.getSkillMarkdownSource(skillName);\n }\n\n deleteManagedSkill(skillId: string): void {\n removeSkillsLockEntry(skillId);\n deleteManagedSkillDir(skillId);\n this.agentService.refreshSkillsAfterDiskChange();\n }\n\n installManagedSkillZip(\n buffer: Buffer,\n opts: { skillId?: string; overwrite?: boolean },\n ): { skillId: string; path: string } {\n const result = installSkillFromZip(buffer, opts);\n removeSkillsLockEntry(result.skillId);\n this.agentService.refreshSkillsAfterDiskChange();\n return result;\n }\n\n async fetchSkillsMarketplaceCatalog(params: SkillsStoreListParams): Promise<SkillsStoreListResponse> {\n const base = resolveSkillsStoreBaseUrl(this.config);\n return listSkillPackages(base, params);\n }\n\n async fetchSkillsMarketplacePackageDetail(packageName: string): Promise<MarketplacePackageDetail> {\n const base = resolveSkillsStoreBaseUrl(this.config);\n return fetchMarketplacePackageDetail(base, packageName);\n }\n\n async installSkillFromMarketplace(opts: {\n name: string;\n version?: string;\n overwrite?: boolean;\n }): Promise<{ skillId: string; path: string }> {\n const base = resolveSkillsStoreBaseUrl(this.config);\n const { downloadUrl } = await resolveSkillZipDownloadUrl(base, opts.name, opts.version);\n const buf = await downloadSkillZipBuffer(base, downloadUrl);\n const skillId = skillIdForMarketplaceInstall(opts.name);\n return this.installManagedSkillZip(buf, { skillId, overwrite: opts.overwrite ?? false });\n }\n\n reloadSkillsFromDisk(): void {\n this.agentService.refreshSkillsAfterDiskChange();\n }\n\n patchSkillEnabled(skillName: string, enabled: boolean): void {\n createSkillConfigManager(resolveStateDir()).setSkillEnabled(skillName, enabled);\n this.agentService.refreshSkillsAfterSkillConfigChange();\n }\n\n get sessionManagerInstance(): SessionManager {\n return this.sessionManager;\n }\n\n async getSessionAgentConfig(sessionKey: string) {\n return this.agentService.getSessionAgentConfig(sessionKey);\n }\n\n /** Resolved markdown workspace for a session (after hydration / mkdir). Used by workspace file API when `sessionKey` is passed. */\n async getEffectiveWorkspacePathForSession(sessionKey: string): Promise<string> {\n return this.agentService.getEffectiveWorkspacePathForSession(sessionKey);\n }\n\n async patchSessionAgentConfig(sessionKey: string, body: {\n thinkingLevel?: string;\n model?: string | null;\n reasoningLevel?: string;\n workingDirectory?: string;\n }) {\n return this.agentService.patchSessionAgentConfig(sessionKey, body);\n }\n\n /**\n * Process a message directly through the agent (for CLI mode)\n */\n async processDirect(content: string, sessionKey = 'cli:direct'): Promise<string> {\n return this.agentService.processDirect(content, sessionKey);\n }\n\n // ========== SSE Event System ==========\n\n /**\n * Subscribe to server-pushed events.\n * Returns a cleanup function to unsubscribe.\n */\n subscribe(sessionId: string, listener: EventListener): () => void {\n this.subscribers.set(sessionId, listener);\n if (!this.eventBuffers.has(sessionId)) {\n this.eventBuffers.set(sessionId, []);\n }\n log.debug({ sessionId }, 'Event subscriber added');\n\n return () => {\n this.subscribers.delete(sessionId);\n // Keep buffer for a while in case they reconnect\n setTimeout(() => {\n if (!this.subscribers.has(sessionId)) {\n this.eventBuffers.delete(sessionId);\n }\n }, 5 * 60_000); // 5 min grace\n log.debug({ sessionId }, 'Event subscriber removed');\n };\n }\n\n /**\n * Emit an event to all subscribers.\n */\n emit(type: string, payload: unknown): void {\n const id = String(++this.eventCounter);\n const event: ServiceEvent = { id, type, payload };\n\n for (const [sessionId, listener] of this.subscribers) {\n // Buffer the event\n const buf = this.eventBuffers.get(sessionId) || [];\n buf.push(event);\n if (buf.length > EVENT_BUFFER_SIZE) buf.shift();\n this.eventBuffers.set(sessionId, buf);\n\n // Deliver\n try {\n listener(event);\n } catch (err) {\n log.warn({ sessionId, err }, 'Failed to deliver event to subscriber');\n }\n }\n }\n\n /**\n * Get events since a given event id (for Last-Event-ID reconnection).\n */\n getEventsSince(sessionId: string, lastEventId: string): ServiceEvent[] {\n const buf = this.eventBuffers.get(sessionId);\n if (!buf) return [];\n\n const idx = buf.findIndex((e) => e.id === lastEventId);\n if (idx === -1) return buf; // can't find cursor — send everything in buffer\n return buf.slice(idx + 1);\n }\n\n /**\n * Save user message to session for webchat (fire-and-forget).\n * Called at the start of runAgent to ensure message survives page refresh.\n */\n private async _saveUserMessage(\n sessionKey: string,\n message: string,\n attachments?: Array<{\n type: string;\n mimeType?: string;\n data?: string;\n name?: string;\n size?: number;\n workspaceRelativePath?: string;\n }>,\n ): Promise<void> {\n // Load existing messages\n const existingMessages = await this.sessionManager.loadMessages(sessionKey);\n\n // Build user message (marker stripped in SessionStore.convertMessages for API clients)\n const userMessage = {\n role: 'user' as const,\n content: [{ type: 'text' as const, text: message }],\n attachments: attachments?.map((a) => ({\n type: a.type,\n mimeType: a.mimeType,\n name: a.name,\n size: a.size,\n workspaceRelativePath: a.workspaceRelativePath,\n // Note: we don't store data (base64) to keep session file small\n })),\n timestamp: Date.now(),\n /** Dropped before `agent.prompt` so we don't duplicate the turn; not exposed via GET session */\n webchatEarlySave: true as const,\n };\n\n // Append and save\n const updatedMessages = [...existingMessages, userMessage];\n await this.sessionManager.saveMessages(sessionKey, updatedMessages);\n log.debug({ sessionKey, messageCount: updatedMessages.length }, 'User message saved');\n }\n\n // ========== Session Management API ==========\n\n /**\n * List sessions with query filters\n */\n async listSessions(query?: SessionListQuery) {\n return this.sessionManager.listSessions(query);\n }\n\n /**\n * List all subagent sessions.\n * Subagent sessions have keys starting with 'subagent:'.\n */\n async listSubagents(query?: SessionListQuery) {\n return this.sessionManager.listSubagents(query);\n }\n\n /**\n * Get a single session by key\n */\n async getSession(key: string) {\n return this.sessionManager.getSession(key);\n }\n\n /**\n * Delete a session\n */\n async deleteSession(key: string): Promise<{ deleted: boolean }> {\n const result = await this.sessionManager.deleteSession(key);\n return { deleted: result };\n }\n\n /**\n * Delete multiple sessions\n */\n async deleteSessions(keys: string[]): Promise<{ success: string[]; failed: string[] }> {\n return this.sessionManager.deleteSessions(keys);\n }\n\n /**\n * Rename a session\n */\n async renameSession(key: string, name: string): Promise<{ renamed: boolean }> {\n await this.sessionManager.renameSession(key, name);\n return { renamed: true };\n }\n\n /**\n * Tag a session\n */\n async tagSession(key: string, tags: string[]): Promise<{ tagged: boolean }> {\n await this.sessionManager.tagSession(key, tags);\n return { tagged: true };\n }\n\n /**\n * Remove tags from a session\n */\n async untagSession(key: string, tags: string[]): Promise<{ untagged: boolean }> {\n await this.sessionManager.untagSession(key, tags);\n return { untagged: true };\n }\n\n /**\n * Archive a session\n */\n async archiveSession(key: string): Promise<{ archived: boolean }> {\n await this.sessionManager.archiveSession(key);\n return { archived: true };\n }\n\n /**\n * Unarchive a session\n */\n async unarchiveSession(key: string): Promise<{ unarchived: boolean }> {\n await this.sessionManager.unarchiveSession(key);\n return { unarchived: true };\n }\n\n /**\n * Pin a session\n */\n async pinSession(key: string): Promise<{ pinned: boolean }> {\n await this.sessionManager.pinSession(key);\n return { pinned: true };\n }\n\n /**\n * Unpin a session\n */\n async unpinSession(key: string): Promise<{ unpinned: boolean }> {\n await this.sessionManager.unpinSession(key);\n return { unpinned: true };\n }\n\n /**\n * Search sessions\n */\n async searchSessions(query: string) {\n return this.sessionManager.searchSessions(query);\n }\n\n /**\n * Search within a session\n */\n async searchInSession(key: string, keyword: string) {\n return this.sessionManager.searchInSession(key, keyword);\n }\n\n /**\n * Export a session\n */\n async exportSession(key: string, format: ExportFormat): Promise<{ content: string }> {\n const content = await this.sessionManager.exportSession(key, format);\n return { content };\n }\n\n /**\n * Get session statistics\n */\n async getSessionStats() {\n return this.sessionManager.getStats();\n }\n\n /**\n * Get unique chat IDs from sessions, grouped by channel\n * Returns a list of channel/chatId pairs for cron job configuration.\n * `chatId` is the session-store routing suffix (unique per bot account + peer).\n * When `routing` exists, `peerId` is the platform id (e.g. Telegram numeric chat id).\n */\n async getSessionChatIds(channel?: string): Promise<\n Array<{\n channel: string;\n chatId: string;\n lastActive: string;\n accountId?: string;\n peerKind?: string;\n peerId?: string;\n }>\n > {\n const result = await this.sessionManager.listSessions({\n limit: 1000,\n sortBy: 'lastAccessedAt',\n sortOrder: 'desc',\n ...(channel ? { channel } : {}),\n });\n\n // Group by channel:chatId to get unique pairs\n const seen = new Set<string>();\n const chatIds: Array<{\n channel: string;\n chatId: string;\n lastActive: string;\n accountId?: string;\n peerKind?: string;\n peerId?: string;\n }> = [];\n\n for (const session of result.items) {\n const key = `${session.sourceChannel}:${session.sourceChatId}`;\n if (!seen.has(key) && session.sourceChannel && session.sourceChatId) {\n seen.add(key);\n const r = session.routing;\n chatIds.push({\n channel: session.sourceChannel,\n chatId: session.sourceChatId,\n lastActive: session.lastAccessedAt,\n ...(r\n ? {\n accountId: r.accountId,\n peerKind: r.peerKind,\n peerId: r.peerId,\n }\n : {}),\n });\n }\n }\n\n return chatIds;\n }\n\n /**\n * Validate authentication token from request headers.\n * Returns true if auth is disabled (mode: 'none') or token is valid.\n */\n validateAuth(headers?: Record<string, string | string[] | undefined>): boolean {\n const token = extractToken(headers);\n return validateToken(this.auth, token);\n }\n\n /**\n * Get current auth mode.\n */\n getAuthMode(): 'none' | 'token' {\n return this.auth.mode;\n }\n\n /**\n * Get current auth token (for CLI server integration).\n * Returns undefined if mode is 'none'.\n */\n getAuthToken(): string | undefined {\n return this.auth.mode === 'token' ? this.auth.token : undefined;\n }\n\n /**\n * Refresh (regenerate) the gateway auth token.\n * Returns the new token.\n */\n async refreshAuthToken(): Promise<string> {\n if (this.auth.mode !== 'token') {\n throw new Error('Cannot refresh token: auth mode is not token');\n }\n\n // Generate new token\n const newToken = crypto.randomBytes(24).toString('hex');\n \n // Update in-memory auth\n this.auth.token = newToken;\n \n // Update config\n this.config = {\n ...this.config,\n gateway: {\n ...this.config.gateway,\n auth: {\n ...this.config.gateway?.auth,\n mode: 'token',\n token: newToken,\n },\n },\n };\n \n await this.writeConfigAndReloadFromDisk(this.config);\n\n log.info({ tokenPreview: `${newToken.slice(0, 8)}...` }, 'Gateway token refreshed');\n \n return newToken;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"service.js","names":["writeConfigToDisk"],"sources":["../../../src/gateway/service.ts"],"sourcesContent":["import crypto from 'crypto';\nimport { AgentService } from '../agent/service.js';\nimport { ChannelManager } from '../channels/manager.js';\nimport { CHAT_CHANNEL_ORDER } from '../channels/registry.js';\nimport { MessageBus, MessageBusShutdownError } from '../infra/bus/index.js';\nimport type { Config as SurfaceConfig } from '../config/config-surface.js';\nimport { loadConfig, saveConfig as writeConfigToDisk } from '../config/index.js';\nimport { getWorkspacePath } from '../config/schema.js';\nimport { CronService } from '../cron/index.js';\nimport { computeBundledExtensionExtensionsPatch } from '../extensions/bundled-extension-activation.js';\nimport { ExtensionLoader } from '../extensions/index.js';\nimport { HeartbeatService, heartbeatRunnerConfigFromConfig } from './heartbeat/index.js';\nimport { ConfigHotReloader } from '../config/reload.js';\nimport { SessionManager } from '../session/index.js';\nimport type { Config } from '../config/schema.js';\nimport type { SessionListQuery, ExportFormat } from '../session/types.js';\nimport { resolveGatewayAuth, assertGatewayAuthConfigured, validateToken, extractToken, type ResolvedGatewayAuth } from './auth.js';\nimport { getModelRegistry } from '../providers/index.js';\nimport {\n createLogger,\n getLogDir,\n getLogStats,\n inboundCorrelationMetadataFromAsyncLogContext,\n} from '../utils/logger.js';\nimport {\n resolveConfigPath,\n resolveCronJobsPath,\n resolveStateDir,\n resolveAgentDir,\n resolveWorkspaceExtensionsDir,\n} from '../config/paths.js';\nimport { AgentRunRelay, type RelayEvent } from './agent-run-relay.js';\nimport { ClarifyBridge, type ClarifyBridgeRequest } from './clarify-bridge.js';\nimport { registerClarifyBridge } from './clarify-runtime.js';\nimport {\n deleteManagedSkill as deleteManagedSkillDir,\n installSkillFromZip,\n listManagedSkillDirs,\n} from '../agent/skills/managed-store.js';\nimport {\n downloadSkillZipBuffer,\n fetchMarketplacePackageDetail,\n listSkillPackages,\n resolveSkillZipDownloadUrl,\n resolveSkillsStoreBaseUrl,\n skillIdForMarketplaceInstall,\n type MarketplacePackageDetail,\n type SkillsStoreListParams,\n type SkillsStoreListResponse,\n} from '../agent/skills/skills-store-client.js';\nimport { createSkillConfigManager } from '../agent/skills/config.js';\nimport { removeSkillsLockEntry } from '../agent/skills/hub-lock.js';\nimport type { SkillCatalogEntry } from '../agent/agent-manager.js';\nimport type { ManagedSkillListItem } from '../agent/skills/managed-store.js';\n\nconst log = createLogger('GatewayService');\nimport { PACKAGE_VERSION } from '../package-version.js';\nimport { buildSessionKey, parseSessionKey } from '../routing/session-key.js';\nimport { getDefaultAgentId } from '../routing/resolve-route.js';\nimport { prependEnvelopeTimestamp } from '../channels/envelope-timestamp.js';\nimport { scheduleGatewayUpdateCheck } from '../infra/update-startup.js';\nimport { restartGatewayProcessWithFreshPid } from './respawn.js';\nimport { MAX_CHAT_ATTACHMENTS } from './chat-limits.js';\n\n// ========== SSE Event System ==========\n\nexport interface ServiceEvent {\n id: string;\n type: string;\n payload: unknown;\n}\n\ntype EventListener = (event: ServiceEvent) => Promise<void> | void;\n\nconst EVENT_BUFFER_SIZE = 200; // ring buffer per subscriber for Last-Event-ID replay\n\nexport interface GatewayServiceConfig {\n configPath?: string;\n enableHotReload?: boolean;\n}\n\nexport class GatewayService {\n private bus: MessageBus;\n private config: Config;\n private configPath: string;\n private agentService: AgentService;\n private channelManager: ChannelManager;\n private cronService: CronService;\n private extensionLoader: ExtensionLoader | null = null;\n private heartbeatService: HeartbeatService;\n private sessionManager: SessionManager;\n private running = false;\n private configReloader: ConfigHotReloader | null = null;\n private startTime = Date.now();\n private workspacePath: string;\n\n // Authentication\n private auth: ResolvedGatewayAuth;\n\n // SSE event system\n private eventCounter = 0;\n private subscribers = new Map<string, EventListener>();\n private eventBuffers = new Map<string, ServiceEvent[]>();\n\n // Agent run relay for resuming SSE streams\n public readonly runRelay = new AgentRunRelay();\n\n /** Per-run abort for webchat (POST /api/agent/abort or client disconnect). */\n private runAbortControllers = new Map<string, AbortController>();\n\n private stopGatewayUpdateCheck: (() => void) | null = null;\n\n /** When set (e.g. by `GatewayServer`), `triggerGatewayProcessRestart` can stop HTTP then exit. */\n private gatewayShutdownForRestart: (() => Promise<void>) | null = null;\n\n private readonly clarifyBridge = new ClarifyBridge();\n\n /** Maps webchat session key → active `runId` for `clarify` tool routing. */\n private activeWebchatRunBySession = new Map<string, string>();\n\n constructor(private serviceConfig: GatewayServiceConfig = {}) {\n this.bus = new MessageBus();\n this.configPath = serviceConfig.configPath || resolveConfigPath();\n this.config = loadConfig(this.configPath);\n\n // Initialize authentication\n this.auth = resolveGatewayAuth({\n authConfig: this.config.gateway?.auth,\n });\n\n // Validate auth configuration\n assertGatewayAuthConfigured(this.auth);\n\n // Log token info (not the token itself)\n if (this.auth.mode === 'token') {\n const tokenPreview = this.auth.token ? `${this.auth.token.slice(0, 4)}***` : 'none';\n log.info({ mode: this.auth.mode, token: tokenPreview }, 'Authentication configured');\n } else {\n log.info({ mode: this.auth.mode }, 'Authentication disabled');\n }\n\n // Initialize channel manager\n this.channelManager = new ChannelManager(this.config, this.bus);\n\n // Initialize extension loader\n this.workspacePath = getWorkspacePath(this.config) || './workspace';\n this.initializeExtensionLoader();\n\n // Initialize ModelRegistry (loads from models.json)\n const registry = getModelRegistry();\n log.debug({ \n modelCount: registry.getAll().length, \n error: registry.getError() || 'none' \n }, 'ModelRegistry initialized');\n\n // Initialize agent service with extension registry\n const modelConfig = this.config.agents?.defaults?.model;\n const cronRef: { service?: CronService } = {};\n this.agentService = new AgentService(this.bus, {\n workspace: this.workspacePath,\n model: typeof modelConfig === 'string' ? modelConfig : modelConfig?.primary,\n config: this.config,\n extensionRegistry: this.extensionLoader?.getRegistry(),\n getCronService: () => cronRef.service,\n gatewayClarify: {\n requestClarification: (sessionKey, request) => {\n const runId = this.activeWebchatRunBySession.get(sessionKey);\n const publishSse = runId\n ? (e: RelayEvent) => {\n this.agentService.enqueueWebchatSseEvent(sessionKey, e);\n }\n : undefined;\n const parsed = parseSessionKey(sessionKey);\n const deliver =\n parsed?.source === 'telegram'\n ? async (ctx: { sessionKey: string; requestId: string; request: ClarifyBridgeRequest }) => {\n await this.deliverTelegramClarify(ctx);\n }\n : undefined;\n if (!runId && !deliver) {\n return Promise.reject(\n new Error('Clarify is not available for this session (use webchat, Telegram, or CLI)'),\n );\n }\n return this.clarifyBridge.startRequest({\n sessionKey,\n runId,\n relay: this.runRelay,\n publishSse,\n request,\n deliver,\n });\n },\n },\n });\n\n\n // Set channel manager reference for model switching\n this.agentService.setChannelManager(this.channelManager);\n this.channelManager.setSessionModelHooks({\n getModelForSession: (sk) => this.agentService.getModelForSession(sk),\n switchModelForSession: (sk, id) => this.agentService.switchModelForSession(sk, id),\n });\n\n // Initialize cron service\n this.cronService = new CronService({\n filePath: resolveCronJobsPath(),\n agentService: this.agentService,\n messageBus: this.bus,\n });\n cronRef.service = this.cronService;\n\n // Initialize session manager\n this.sessionManager = new SessionManager({\n config: this.config,\n });\n\n this.heartbeatService = new HeartbeatService({\n agentService: this.agentService,\n messageBus: this.bus,\n cronService: this.cronService,\n sessionStore: this.sessionManager.getStore(),\n getConfig: () => this.config,\n });\n\n this.cronService.setDeps({\n agentService: this.agentService,\n messageBus: this.bus,\n heartbeatService: this.heartbeatService,\n getDefaultCronAgentId: () => getDefaultAgentId(this.config),\n });\n }\n\n /**\n * Create extension loader and resolve configs (load runs in start() before channels).\n */\n private initializeExtensionLoader(): void {\n try {\n const aid = getDefaultAgentId(this.config);\n this.extensionLoader = new ExtensionLoader({\n workspaceDir: this.workspacePath,\n extensionsDir: resolveWorkspaceExtensionsDir(this.config, aid),\n });\n this.extensionLoader.setConfig(this.config as Parameters<ExtensionLoader['setConfig']>[0]);\n } catch (error) {\n log.warn({ error }, 'Failed to initialize extension loader');\n }\n }\n\n /**\n * Load extensions and register SDK / full ChannelPlugin instances with ChannelManager.\n */\n private async loadExtensionsAndRegisterChannels(): Promise<void> {\n if (!this.extensionLoader) {\n return;\n }\n try {\n await this.extensionLoader.loadByActivationPlan();\n const reg = this.extensionLoader.getRegistry();\n for (const plugin of reg.channelPlugins) {\n this.channelManager.registerPlugin(plugin);\n }\n log.debug(\n {\n extensionRecords: reg.extensions.size,\n channelPlugins: reg.channelPlugins.length,\n },\n 'Extensions loaded and channel plugins registered',\n );\n } catch (err) {\n log.warn({ err }, 'Failed to load extensions');\n }\n }\n\n async start(): Promise<void> {\n if (this.running) return;\n\n log.debug('Starting gateway service...');\n this.startTime = Date.now();\n this.running = true;\n\n registerClarifyBridge(this.clarifyBridge);\n\n this.channelManager.setOutboundHooks({\n runMessageSending: (to, content, channel) =>\n this.agentService.invokeOutboundMessageSending(to, content, channel),\n runMessageSent: (to, content, success, error, channel) =>\n this.agentService.invokeOutboundMessageSent(to, content, success, error, channel),\n });\n this.channelManager.enableOutboundPersistence(resolveAgentDir(this.config, getDefaultAgentId(this.config)));\n\n if (this.extensionLoader) {\n this.extensionLoader.setRuntimeContext({\n bus: this.bus,\n sessionManager: this.sessionManager,\n });\n }\n\n await this.loadExtensionsAndRegisterChannels();\n\n // Start channels (initialize first, then start)\n await this.channelManager.initialize();\n await this.channelManager.start();\n await this.channelManager.replayPendingOutboundMessages();\n\n // Initialize session manager\n await this.sessionManager.initialize();\n log.debug('Session manager initialized');\n\n this.cronService.setDeps({\n agentService: this.agentService,\n messageBus: this.bus,\n heartbeatService: this.heartbeatService,\n sessionStore: this.sessionManager.getStore(),\n getDefaultCronAgentId: () => getDefaultAgentId(this.config),\n });\n\n this.sessionManager.on('sessionUpdated', (data: { key: string; name?: string; tags?: string[] }) => {\n this.emit('session.updated', { key: data.key, name: data.name, tags: data.tags });\n });\n\n // Start cron service\n if (this.config.cron?.enabled !== false) {\n await this.cronService.initialize();\n }\n\n this.heartbeatService.start(heartbeatRunnerConfigFromConfig(this.config));\n\n // Start agent service (runs in background)\n this.agentService.start().catch((err) => {\n log.error({ err }, 'Agent service error');\n });\n\n // Start outbound message processor\n this.startOutboundProcessor().catch((err) => {\n log.error({ err }, 'Outbound processor error');\n });\n\n // Setup config hot reload\n if (this.serviceConfig.enableHotReload !== false) {\n this.setupConfigReloader();\n }\n\n this.stopGatewayUpdateCheck = scheduleGatewayUpdateCheck({\n config: this.config,\n onUpdateAvailableChange: (update) => {\n this.emit('update.available', update);\n },\n });\n\n log.debug('Gateway service started');\n }\n\n async stop(): Promise<void> {\n if (!this.running) return;\n\n log.debug('Stopping gateway service...');\n\n if (this.stopGatewayUpdateCheck) {\n this.stopGatewayUpdateCheck();\n this.stopGatewayUpdateCheck = null;\n }\n\n // Stop config reloader\n if (this.configReloader) {\n await this.configReloader.stop();\n this.configReloader = null;\n }\n\n // Stop heartbeat service\n this.heartbeatService.stop();\n\n registerClarifyBridge(null);\n this.clarifyBridge.dispose();\n this.agentService.stop();\n\n // Unblock `consumeOutbound()` / `consumeInbound()` waiters before stopping channels (CLI agent does the same).\n this.running = false;\n this.bus.shutdown();\n\n await this.channelManager.stop();\n\n // Stop cron service\n await this.cronService.stop();\n\n log.debug('Gateway service stopped');\n }\n\n /**\n * Start processing outbound messages and send through channels\n */\n private async startOutboundProcessor(): Promise<void> {\n log.debug('Starting outbound message processor');\n while (this.running) {\n try {\n const msg = await this.bus.consumeOutbound();\n await this.channelManager.send(msg);\n } catch (error) {\n if (error instanceof MessageBusShutdownError) {\n break;\n }\n const em = error instanceof Error ? error.message : String(error);\n log.error(\n { err: error, errorMessage: em, phase: 'outbound_consume' },\n `Outbound pipeline failed (will retry in 1s): ${em}`,\n );\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n }\n }\n\n /**\n * Setup config hot reload using ConfigHotReloader\n */\n private setupConfigReloader(): void {\n this.configReloader = new ConfigHotReloader(\n this.configPath,\n this.config,\n {\n onModelsReload: (newConfig) => this.handleModelsReload(newConfig),\n onAgentDefaultsReload: (newConfig) => this.handleAgentDefaultsReload(newConfig),\n onChannelsReload: (newConfig) => this.handleChannelsReload(newConfig),\n onCronReload: (newConfig) => this.handleCronReload(newConfig),\n onHeartbeatReload: (newConfig) => this.handleHeartbeatReload(newConfig),\n onToolsReload: (newConfig) => this.handleToolsReload(newConfig),\n onExtensionsReload: async (newConfig, changedPaths) => {\n await this.handleExtensionsReload(newConfig, changedPaths);\n },\n onFullRestart: (newConfig) => {\n log.warn(\n { requiresProcessRestart: true, hint: 'Restart the gateway process (hot reload cannot apply this change).' },\n 'Config reload: full gateway restart required — see prior \"restartPaths\" info log',\n );\n this.config = newConfig;\n this.emit('config.reload', { section: 'full', requiresRestart: true });\n },\n },\n {\n debounceMs: 300,\n enabled: this.serviceConfig.enableHotReload !== false,\n }\n );\n this.configReloader.start();\n }\n\n /**\n * Handle models config hot reload\n */\n private handleModelsReload(newConfig: Config): void {\n log.debug('Reloading models config...');\n this.config = newConfig;\n getModelRegistry().refresh();\n this.emit('config.reload', { section: 'models' });\n log.debug('Models config reloaded');\n }\n\n /**\n * Handle agent defaults config hot reload\n */\n private handleAgentDefaultsReload(newConfig: Config): void {\n log.debug('Reloading agent defaults...');\n this.config = newConfig;\n this.agentService.applyAgentDefaultsFromConfig(newConfig);\n this.emit('config.reload', { section: 'agents' });\n log.debug('Agent defaults reloaded');\n }\n\n /**\n * Apply `latest.channels` to every registered channel plugin (Telegram, Weixin, extensions).\n * Single runtime path for: file watcher hot reload, API saves, and Weixin QR follow-up.\n */\n private async handleChannelsReload(newConfig: Config): Promise<void> {\n log.debug('Reloading channels config...');\n this.config = newConfig;\n await this.channelManager.updateConfig(newConfig);\n this.emit('config.reload', { section: 'channels' });\n this.emit('channels.status', { channels: this.getChannelsStatus() });\n log.debug('Channels config reloaded');\n }\n\n /** After persisting config to disk: align plugins + debounced reload baseline (watchers skip duplicate diffs). */\n private async syncChannelPluginsAfterPersist(): Promise<void> {\n await this.handleChannelsReload(this.config);\n this.configReloader?.syncCurrentConfig(this.config);\n }\n\n /**\n * Handle cron config hot reload\n */\n private handleCronReload(newConfig: Config): void {\n log.debug('Reloading cron config...');\n this.config = newConfig;\n this.cronService.updateConfig(newConfig);\n this.emit('config.reload', { section: 'cron' });\n log.debug('Cron config reloaded');\n }\n\n /**\n * Handle heartbeat config hot reload\n */\n private handleHeartbeatReload(newConfig: Config): void {\n log.debug('Reloading heartbeat config...');\n this.config = newConfig;\n this.heartbeatService.updateConfig(newConfig);\n this.emit('config.reload', { section: 'heartbeat' });\n log.debug('Heartbeat config reloaded');\n }\n\n /**\n * Apply `gateway.heartbeat` from current config after PATCH /api/config (and when hot reload is off).\n * File watcher uses `handleHeartbeatReload` with the same effect when paths match.\n */\n reloadHeartbeatFromCurrentConfig(): void {\n this.handleHeartbeatReload(this.config);\n }\n\n /**\n * Handle tools config hot reload\n */\n private handleToolsReload(newConfig: Config): void {\n log.debug('Reloading tools config...');\n this.config = newConfig;\n this.emit('config.reload', { section: 'tools' });\n log.debug('Tools config reloaded');\n }\n\n /**\n * Dispatch config hot reload to extensions that registered `registerReload`, matching changed paths.\n */\n private async handleExtensionsReload(\n newConfig: Config,\n changedPaths: string[],\n ): Promise<void> {\n this.config = newConfig;\n this.extensionLoader?.setConfig(this.config as unknown as SurfaceConfig);\n\n if (!this.extensionLoader) {\n this.emit('config.reload', {\n section: 'extensions',\n source: 'extension-reload',\n changedPaths,\n });\n return;\n }\n\n const registry = this.extensionLoader.getRegistry();\n const matchingRegs = registry.getMatchingReloadRegistrations(changedPaths);\n\n if (matchingRegs.length === 0) {\n log.debug({ changedPaths }, 'No extension reload handlers matched');\n this.emit('config.reload', {\n section: 'extensions',\n source: 'extension-reload',\n changedPaths,\n });\n return;\n }\n\n for (const reg of matchingRegs) {\n const relevantPaths = changedPaths.filter(\n (p) =>\n reg.configPrefixes.length === 0 ||\n reg.configPrefixes.some(\n (prefix) => p === prefix || p.startsWith(`${prefix}.`),\n ),\n );\n\n log.info(\n { extensionId: reg.extensionId, relevantPaths },\n 'Calling extension reload handler',\n );\n\n try {\n const result = await reg.handler(newConfig, relevantPaths);\n if (result.success) {\n log.info({ extensionId: reg.extensionId }, 'Extension reload succeeded');\n } else {\n log.warn(\n { extensionId: reg.extensionId, error: result.error },\n `Extension reload reported failure: ${result.error ?? 'unknown'}`,\n );\n }\n } catch (err) {\n const errorMessage = err instanceof Error ? err.message : String(err);\n log.error(\n { err, extensionId: reg.extensionId, errorMessage },\n `Extension reload handler threw: ${errorMessage}`,\n );\n }\n }\n\n this.emit('config.reload', {\n section: 'extensions',\n source: 'extension-reload',\n changedPaths,\n });\n }\n\n /**\n * Reload configuration from disk (manual trigger)\n */\n async reloadConfig(): Promise<{ reloaded: boolean; error?: string }> {\n if (!this.configReloader) {\n return { reloaded: false, error: 'Config reloader not initialized' };\n }\n const result = await this.configReloader.triggerReload();\n return { reloaded: result.success, error: result.error };\n }\n\n /**\n * After Weixin QR login: token files may change without a `channels.weixin` JSON diff, so run the same\n * channel apply as an API save, then force Weixin long-poll restart (see `reloadMonitorsWithConfig`).\n */\n async afterWeixinCredentialsPersisted(): Promise<void> {\n const next = loadConfig(this.configPath);\n this.config = next;\n this.agentService.applyAgentDefaultsFromConfig(next);\n this.configReloader?.syncCurrentConfig(next);\n await this.handleChannelsReload(next);\n const { weixinPlugin } = await import('../channels/weixin/index.js');\n await weixinPlugin.reloadMonitorsWithConfig(this.config, this.bus);\n log.info('Weixin monitors restarted after credential login');\n }\n\n /**\n * After Feishu WebUI QR setup: `channels.feishu` was written directly to disk; reload into memory\n * and apply channel plugins (same baseline as PATCH /api/config).\n */\n async afterFeishuCredentialsPersisted(): Promise<void> {\n const next = loadConfig(this.configPath);\n this.config = next;\n this.agentService.applyAgentDefaultsFromConfig(next);\n this.configReloader?.syncCurrentConfig(next);\n await this.handleChannelsReload(next);\n log.info('Feishu config applied after QR setup');\n }\n\n /**\n * Save current config to disk\n */\n /**\n * Persist and replace `this.config` with the validated file contents so runtime matches disk\n * (PATCH merge objects can drift from Zod-normalized output).\n */\n private async writeConfigAndReloadFromDisk(configToWrite: Config): Promise<void> {\n await writeConfigToDisk(configToWrite, this.configPath);\n this.config = loadConfig(this.configPath);\n this.agentService.applyAgentDefaultsFromConfig(this.config);\n }\n\n async saveConfig(config: Config): Promise<{ saved: boolean; error?: string }> {\n try {\n await this.writeConfigAndReloadFromDisk(config);\n await this.syncChannelPluginsAfterPersist();\n return { saved: true };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n log.error({ error }, 'Failed to save config');\n return { saved: false, error };\n }\n }\n\n /**\n * App store (phase 1): persist `extensions.enabled` / `extensions.disabled` for a bundled extension.\n * Extension modules are loaded at gateway startup; restart the gateway process to fully apply load/unload.\n */\n async setBundledExtensionActivationTarget(\n extensionId: string,\n wanted: boolean,\n ): Promise<{ ok: boolean; error?: string; requiresGatewayRestart: boolean }> {\n const loader = this.extensionLoader;\n if (!loader) {\n return { ok: false, error: 'Extension loader unavailable', requiresGatewayRestart: false };\n }\n const id = extensionId.trim();\n if (!id) {\n return { ok: false, error: 'Invalid extension id', requiresGatewayRestart: false };\n }\n const patch = computeBundledExtensionExtensionsPatch(loader, this.config, id, wanted);\n if (patch.ok === false) {\n return { ok: false, error: patch.error, requiresGatewayRestart: false };\n }\n const newConfig = { ...this.config, extensions: patch.extensions } as Config;\n const saved = await this.saveConfig(newConfig);\n if (!saved.saved) {\n return { ok: false, error: saved.error ?? 'Failed to save config', requiresGatewayRestart: false };\n }\n loader.setConfig(this.config as unknown as SurfaceConfig);\n return { ok: true, requiresGatewayRestart: true };\n }\n\n /**\n * Update configuration and persist to disk\n */\n async updateConfig(updates: Partial<Config>): Promise<{ updated: boolean; error?: string }> {\n try {\n log.debug('Updating configuration...');\n \n // Merge updates\n this.config = { ...this.config, ...updates };\n\n await this.writeConfigAndReloadFromDisk(this.config);\n await this.syncChannelPluginsAfterPersist();\n\n log.debug('Configuration updated successfully');\n return { updated: true };\n } catch (err) {\n const error = err instanceof Error ? err.message : String(err);\n log.error({ error }, 'Failed to update config');\n return { updated: false, error };\n }\n }\n\n /**\n * Run agent with a message and stream events\n */\n /**\n * @param runOptions.signal — When set (e.g. client disconnect), aborts in-flight generation and persists partial output.\n */\n async *runAgent(\n message: string,\n channel: string,\n chatId: string,\n attachments?: Array<{\n type: string;\n mimeType?: string;\n data?: string;\n name?: string;\n size?: number;\n }>,\n thinking?: string,\n runOptions?: { signal?: AbortSignal },\n ): AsyncGenerator<{ type: string; content?: string; status?: string; runId?: string }, { status: string; summary: string }, unknown> {\n const cappedAttachments =\n attachments && attachments.length > MAX_CHAT_ATTACHMENTS\n ? attachments.slice(0, MAX_CHAT_ATTACHMENTS)\n : attachments;\n if (attachments && cappedAttachments && attachments.length > cappedAttachments.length) {\n log.debug(\n { dropped: attachments.length - cappedAttachments.length, max: MAX_CHAT_ATTACHMENTS },\n 'Attachments capped for webchat',\n );\n }\n\n const runId = crypto.randomUUID();\n\n // For webchat, register the run in the relay before yielding the first event\n if (channel === 'webchat') {\n const parsedKey = parseSessionKey(chatId);\n const sessionKey = parsedKey ? chatId : buildSessionKey({\n agentId: getDefaultAgentId(this.config),\n source: 'webchat',\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n this.runRelay.ensureRun(runId, sessionKey);\n this.runAbortControllers.set(runId, new AbortController());\n }\n\n const statusEvent = { type: 'status', status: 'accepted', runId };\n if (channel === 'webchat') this.runRelay.publish(runId, statusEvent);\n yield statusEvent;\n\n try {\n // For 'webchat' channel (web UI), process through agent service\n if (channel === 'webchat') {\n // Determine session key: if chatId is already a valid session key, use it directly\n // Otherwise, build a new session key from the chatId\n const parsedKey = parseSessionKey(chatId);\n const sessionKey = parsedKey ? chatId : buildSessionKey({\n agentId: getDefaultAgentId(this.config),\n source: 'webchat',\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n\n const timezone = this.agentService.resolveUserTimezoneForSession(sessionKey);\n // Keep UI clean: persist raw user text (no envelope timestamp),\n // but include a stamped variant for the model so it has a stable \"now\".\n const stampedMessage = prependEnvelopeTimestamp(message, timezone);\n const prepared = await this.agentService.prepareInboundAttachments(sessionKey, cappedAttachments);\n\n // Persist before streaming so a mid-turn refresh still sees text + attachment refs on disk.\n try {\n await this._saveUserMessage(sessionKey, message, prepared);\n } catch (err) {\n log.error({ err, sessionKey }, 'Failed to save user message');\n }\n\n const runAbort = this.runAbortControllers.get(runId);\n if (!runAbort) {\n throw new Error('run abort controller missing for webchat');\n }\n const mergedSignal = runOptions?.signal\n ? AbortSignal.any([runOptions.signal, runAbort.signal])\n : runAbort.signal;\n\n this.activeWebchatRunBySession.set(sessionKey, runId);\n try {\n this.emit('agent.stream', { sessionKey, event: statusEvent });\n const eventStream = this.agentService.processDirectStreaming(stampedMessage, sessionKey, prepared, thinking, {\n signal: mergedSignal,\n });\n\n for await (const event of eventStream) {\n this.runRelay.publish(runId, event);\n this.emit('agent.stream', { sessionKey, event });\n yield event as { type: string; content?: string; status?: string; runId?: string };\n }\n\n this.runRelay.complete(runId);\n try {\n const metaAfter = await this.sessionManager.getSessionMetadata(sessionKey);\n if (metaAfter?.name) {\n this.emit('session.updated', { key: sessionKey, name: metaAfter.name });\n }\n } catch {\n /* ignore */\n }\n return {\n status: mergedSignal.aborted ? 'aborted' : 'ok',\n summary: mergedSignal.aborted ? 'Interrupted' : 'Message processed successfully',\n };\n } catch (error) {\n log.error({ error }, 'Agent processing failed');\n const errorEvent = { type: 'error', content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}` };\n this.runRelay.publish(runId, errorEvent);\n this.emit('agent.stream', { sessionKey, event: errorEvent });\n this.runRelay.complete(runId);\n yield errorEvent;\n return { status: 'error', summary: error instanceof Error ? error.message : 'Unknown error' };\n } finally {\n this.activeWebchatRunBySession.delete(sessionKey);\n this.runAbortControllers.delete(runId);\n }\n }\n\n // Send message through bus for other channels (telegram, etc.)\n const correlationMeta = inboundCorrelationMetadataFromAsyncLogContext();\n await this.bus.publishInbound({\n channel,\n sender_id: 'gateway',\n chat_id: chatId,\n content: message,\n ...(correlationMeta ? { metadata: correlationMeta } : {}),\n });\n\n // Wait for and collect response\n // This is simplified - in production we'd need proper session tracking\n yield { type: 'token', content: 'Processing...\\n' };\n\n // Simulate processing delay\n await new Promise((resolve) => setTimeout(resolve, 1000));\n\n yield { type: 'token', content: 'Done\\n' };\n\n return { status: 'ok', summary: 'Message processed' };\n } catch (error) {\n log.error({ error }, 'Agent run failed');\n throw error;\n }\n }\n\n /** Abort an in-flight webchat agent run (matches `runId` from SSE `status`). */\n abortAgentRun(runId: string): boolean {\n this.clarifyBridge.cancelForRun(runId);\n for (const [sk, id] of this.activeWebchatRunBySession) {\n if (id === runId) {\n this.activeWebchatRunBySession.delete(sk);\n }\n }\n const c = this.runAbortControllers.get(runId);\n if (!c) {\n return false;\n }\n c.abort();\n return true;\n }\n\n /**\n * Queue steering text for an active webchat run (`Agent.steer` / tool-boundary injection).\n * `chatId` is the same as `POST /api/agent` body (`sessionKey` or legacy peer id).\n */\n async steerWebchatAgent(\n chatId: string,\n message: string,\n ): Promise<{ ok: true } | { ok: false; code: 'BAD_REQUEST' | 'NO_ACTIVE_RUN' | 'STEER_FAILED' }> {\n const trimmed = message.trim();\n if (!trimmed) {\n return { ok: false, code: 'BAD_REQUEST' };\n }\n const parsedKey = parseSessionKey(chatId);\n const sessionKey = parsedKey\n ? chatId\n : buildSessionKey({\n agentId: getDefaultAgentId(this.config),\n source: 'webchat',\n accountId: 'default',\n peerKind: 'direct',\n peerId: chatId,\n });\n if (!this.activeWebchatRunBySession.has(sessionKey)) {\n return { ok: false, code: 'NO_ACTIVE_RUN' };\n }\n const steered = await this.agentService.steerWebchatSession(sessionKey, trimmed);\n if (!steered) {\n return { ok: false, code: 'STEER_FAILED' };\n }\n return { ok: true };\n }\n\n private async deliverTelegramClarify(ctx: {\n sessionKey: string;\n requestId: string;\n request: ClarifyBridgeRequest;\n }): Promise<void> {\n const parsed = parseSessionKey(ctx.sessionKey);\n if (!parsed || parsed.source !== 'telegram') {\n return;\n }\n\n let body = ctx.request.question;\n if (ctx.request.default) {\n body += `\\n\\nDefault if unsure: ${ctx.request.default}`;\n }\n\n const choices = ctx.request.choices;\n const buttonRows =\n choices && choices.length >= 2\n ? choices.map((c, i) => [\n {\n text: c.length > 64 ? `${c.slice(0, 61)}…` : c,\n callback_data: `clarify:${ctx.requestId}:${i}`,\n },\n ])\n : undefined;\n\n if (!buttonRows) {\n body += '\\n\\nReply with your answer in this chat.';\n }\n\n await this.channelManager.send({\n channel: 'telegram',\n chat_id: parsed.peerId,\n content: body,\n metadata: {\n accountId: parsed.accountId,\n ...(parsed.threadId ? { threadId: parsed.threadId } : {}),\n },\n buttons: buttonRows,\n });\n }\n\n /** Deliver a user's answer to a pending `clarify` tool call. */\n submitClarifyResponse(requestId: string, answer: string): boolean {\n return this.clarifyBridge.handleResponse(requestId, answer);\n }\n\n /**\n * Send message through a channel\n */\n async sendMessage(\n channel: string,\n chatId: string,\n content: string\n ): Promise<{ sent: boolean; messageId?: string }> {\n try {\n await this.channelManager.send({\n channel,\n chat_id: chatId,\n content,\n });\n const messageId = `msg_${Date.now()}`;\n this.emit('message.sent', { channel, chatId, messageId });\n return { sent: true, messageId };\n } catch (error) {\n log.error({ channel, chatId, error }, 'Failed to send message');\n throw error;\n }\n }\n\n /**\n * Get channel statuses\n */\n getChannelsStatus(): Array<{\n name: string;\n enabled: boolean;\n connected: boolean;\n }> {\n const runningChannels = new Set(this.channelManager.getRunningChannels());\n const channels = this.config.channels as Record<string, { enabled?: boolean } | undefined> | undefined;\n const builtinOrder = CHAT_CHANNEL_ORDER as readonly string[];\n\n const rows: Array<{ name: string; enabled: boolean; connected: boolean }> = CHAT_CHANNEL_ORDER.map(\n (name) => ({\n name,\n enabled: !!channels?.[name]?.enabled,\n connected: runningChannels.has(name),\n }),\n );\n\n const extReg = this.extensionLoader?.getRegistry();\n const extraIds = extReg?.channelPlugins.map((p) => p.id).filter((id) => !builtinOrder.includes(id)) ?? [];\n if (extraIds.length === 0) {\n return rows;\n }\n\n const seen = new Set(builtinOrder);\n for (const name of extraIds) {\n if (seen.has(name)) continue;\n seen.add(name);\n rows.push({\n name,\n enabled: channels?.[name]?.enabled !== false,\n connected: runningChannels.has(name),\n });\n }\n\n return rows;\n }\n\n /**\n * Request an immediate heartbeat run (coalesced like interval/cron wakes).\n */\n requestHeartbeatNow(opts?: { reason?: string }): void {\n this.heartbeatService.requestNow({ reason: opts?.reason ?? 'manual' });\n }\n\n /**\n * Register graceful shutdown used after spawning a replacement gateway process (foreground CLI server).\n */\n registerGatewayShutdownForRestart(handler: () => Promise<void>): void {\n this.gatewayShutdownForRestart = handler;\n }\n\n /**\n * Respawn the gateway process when supported (spawn + exit, supervisor exit, or disabled when XOPC_NO_RESPAWN).\n */\n triggerGatewayProcessRestart(): { ok: boolean; mode: string; message?: string } {\n const result = restartGatewayProcessWithFreshPid();\n if (result.mode === 'failed') {\n return { ok: false, mode: result.mode, message: result.detail ?? 'spawn failed' };\n }\n if (result.mode === 'disabled') {\n return {\n ok: false,\n mode: 'disabled',\n message:\n 'Process respawn is disabled (XOPC_NO_RESPAWN). Restart the gateway manually (e.g. xopc gateway restart).',\n };\n }\n const shutdown = this.gatewayShutdownForRestart;\n if (!shutdown) {\n return {\n ok: false,\n mode: result.mode,\n message: 'Gateway restart is not available in this process.',\n };\n }\n setImmediate(() => {\n void shutdown().finally(() => {\n process.exit(0);\n });\n });\n return { ok: true, mode: result.mode };\n }\n\n /**\n * Get health status\n */\n getHealth(): {\n status: string;\n service: string;\n version: string;\n uptime: number;\n channels: { running: number; total: number };\n configPath: string;\n logs?: {\n dir: string;\n errors24h: number;\n stats: Record<string, number>;\n };\n } {\n const runningChannels = this.channelManager.getRunningChannels();\n const allChannels = this.channelManager.getAllChannels();\n const logStats = getLogStats();\n\n return {\n status: 'ok',\n service: 'xopc-gateway',\n version: PACKAGE_VERSION,\n uptime: Math.floor((Date.now() - this.startTime) / 1000),\n channels: {\n running: runningChannels.length,\n total: allChannels.length,\n },\n configPath: this.configPath,\n logs: {\n dir: getLogDir(),\n errors24h: logStats.errorsLast24h,\n stats: logStats.byLevel,\n },\n };\n }\n\n get isRunning(): boolean {\n return this.running;\n }\n\n /**\n * Get extension registry for external access (HTTP routes, gateway methods)\n */\n getExtensionRegistry() {\n return this.extensionLoader?.getRegistry();\n }\n\n /** Extension loader for discovery and frontend asset APIs (may be null if extensions failed to init). */\n getExtensionLoader(): ExtensionLoader | null {\n return this.extensionLoader;\n }\n\n /**\n * Get model registry for external access (HTTP routes)\n */\n getModelRegistry() {\n const { getModelRegistry } = require('../providers/index.js');\n return getModelRegistry();\n }\n\n /**\n * Invoke a gateway method registered by extensions\n */\n async invokeGatewayMethod(method: string, params: Record<string, unknown>): Promise<unknown> {\n const registry = this.getExtensionRegistry();\n if (!registry) {\n throw new Error('Extension registry not available');\n }\n\n const handler = registry.getGatewayMethod(method);\n if (!handler) {\n throw new Error(`Gateway method not found: ${method}`);\n }\n\n return await handler(params);\n }\n\n get currentConfig(): Config {\n return this.config;\n }\n\n get cronServiceInstance(): CronService {\n return this.cronService;\n }\n\n getSkillsApi(): { catalog: SkillCatalogEntry[]; managed: ManagedSkillListItem[] } {\n return {\n catalog: this.agentService.getSkillCatalog(),\n managed: listManagedSkillDirs(),\n };\n }\n\n getSkillMarkdownSource(skillName: string): { name: string; markdown: string } | null {\n return this.agentService.getSkillMarkdownSource(skillName);\n }\n\n deleteManagedSkill(skillId: string): void {\n removeSkillsLockEntry(skillId);\n deleteManagedSkillDir(skillId);\n this.agentService.refreshSkillsAfterDiskChange();\n }\n\n installManagedSkillZip(\n buffer: Buffer,\n opts: { skillId?: string; overwrite?: boolean },\n ): { skillId: string; path: string } {\n const result = installSkillFromZip(buffer, opts);\n removeSkillsLockEntry(result.skillId);\n this.agentService.refreshSkillsAfterDiskChange();\n return result;\n }\n\n async fetchSkillsMarketplaceCatalog(params: SkillsStoreListParams): Promise<SkillsStoreListResponse> {\n const base = resolveSkillsStoreBaseUrl(this.config);\n return listSkillPackages(base, params);\n }\n\n async fetchSkillsMarketplacePackageDetail(packageName: string): Promise<MarketplacePackageDetail> {\n const base = resolveSkillsStoreBaseUrl(this.config);\n return fetchMarketplacePackageDetail(base, packageName);\n }\n\n async installSkillFromMarketplace(opts: {\n name: string;\n version?: string;\n overwrite?: boolean;\n }): Promise<{ skillId: string; path: string }> {\n const base = resolveSkillsStoreBaseUrl(this.config);\n const { downloadUrl } = await resolveSkillZipDownloadUrl(base, opts.name, opts.version);\n const buf = await downloadSkillZipBuffer(base, downloadUrl);\n const skillId = skillIdForMarketplaceInstall(opts.name);\n return this.installManagedSkillZip(buf, { skillId, overwrite: opts.overwrite ?? false });\n }\n\n reloadSkillsFromDisk(): void {\n this.agentService.refreshSkillsAfterDiskChange();\n }\n\n patchSkillEnabled(skillName: string, enabled: boolean): void {\n createSkillConfigManager(resolveStateDir()).setSkillEnabled(skillName, enabled);\n this.agentService.refreshSkillsAfterSkillConfigChange();\n }\n\n get sessionManagerInstance(): SessionManager {\n return this.sessionManager;\n }\n\n async getSessionAgentConfig(sessionKey: string) {\n return this.agentService.getSessionAgentConfig(sessionKey);\n }\n\n /** Resolved markdown workspace for a session (after hydration / mkdir). Used by workspace file API when `sessionKey` is passed. */\n async getEffectiveWorkspacePathForSession(sessionKey: string): Promise<string> {\n return this.agentService.getEffectiveWorkspacePathForSession(sessionKey);\n }\n\n async patchSessionAgentConfig(sessionKey: string, body: {\n thinkingLevel?: string;\n model?: string | null;\n reasoningLevel?: string;\n workingDirectory?: string;\n }) {\n return this.agentService.patchSessionAgentConfig(sessionKey, body);\n }\n\n /**\n * Process a message directly through the agent (for CLI mode)\n */\n async processDirect(content: string, sessionKey = 'cli:direct'): Promise<string> {\n return this.agentService.processDirect(content, sessionKey);\n }\n\n // ========== SSE Event System ==========\n\n /**\n * Subscribe to server-pushed events.\n * Returns a cleanup function to unsubscribe.\n */\n subscribe(sessionId: string, listener: EventListener): () => void {\n this.subscribers.set(sessionId, listener);\n if (!this.eventBuffers.has(sessionId)) {\n this.eventBuffers.set(sessionId, []);\n }\n log.debug({ sessionId }, 'Event subscriber added');\n\n return () => {\n this.subscribers.delete(sessionId);\n // Keep buffer for a while in case they reconnect\n setTimeout(() => {\n if (!this.subscribers.has(sessionId)) {\n this.eventBuffers.delete(sessionId);\n }\n }, 5 * 60_000); // 5 min grace\n log.debug({ sessionId }, 'Event subscriber removed');\n };\n }\n\n /**\n * Emit an event to all subscribers.\n */\n emit(type: string, payload: unknown): void {\n const id = String(++this.eventCounter);\n const event: ServiceEvent = { id, type, payload };\n\n for (const [sessionId, listener] of this.subscribers) {\n // Buffer the event\n const buf = this.eventBuffers.get(sessionId) || [];\n buf.push(event);\n if (buf.length > EVENT_BUFFER_SIZE) buf.shift();\n this.eventBuffers.set(sessionId, buf);\n\n // Deliver\n try {\n listener(event);\n } catch (err) {\n log.warn({ sessionId, err }, 'Failed to deliver event to subscriber');\n }\n }\n }\n\n /**\n * Get events since a given event id (for Last-Event-ID reconnection).\n */\n getEventsSince(sessionId: string, lastEventId: string): ServiceEvent[] {\n const buf = this.eventBuffers.get(sessionId);\n if (!buf) return [];\n\n const idx = buf.findIndex((e) => e.id === lastEventId);\n if (idx === -1) return buf; // can't find cursor — send everything in buffer\n return buf.slice(idx + 1);\n }\n\n /**\n * Save user message to session for webchat (fire-and-forget).\n * Called at the start of runAgent to ensure message survives page refresh.\n */\n private async _saveUserMessage(\n sessionKey: string,\n message: string,\n attachments?: Array<{\n type: string;\n mimeType?: string;\n data?: string;\n name?: string;\n size?: number;\n workspaceRelativePath?: string;\n }>,\n ): Promise<void> {\n // Load existing messages\n const existingMessages = await this.sessionManager.loadMessages(sessionKey);\n\n // Build user message (marker stripped in SessionStore.convertMessages for API clients)\n const userMessage = {\n role: 'user' as const,\n content: [{ type: 'text' as const, text: message }],\n attachments: attachments?.map((a) => ({\n type: a.type,\n mimeType: a.mimeType,\n name: a.name,\n size: a.size,\n workspaceRelativePath: a.workspaceRelativePath,\n // Note: we don't store data (base64) to keep session file small\n })),\n timestamp: Date.now(),\n /** Dropped before `agent.prompt` so we don't duplicate the turn; not exposed via GET session */\n webchatEarlySave: true as const,\n };\n\n // Append and save\n const updatedMessages = [...existingMessages, userMessage];\n await this.sessionManager.saveMessages(sessionKey, updatedMessages);\n log.debug({ sessionKey, messageCount: updatedMessages.length }, 'User message saved');\n }\n\n // ========== Session Management API ==========\n\n /**\n * List sessions with query filters\n */\n async listSessions(query?: SessionListQuery) {\n return this.sessionManager.listSessions(query);\n }\n\n /**\n * List all subagent sessions.\n * Subagent sessions have keys starting with 'subagent:'.\n */\n async listSubagents(query?: SessionListQuery) {\n return this.sessionManager.listSubagents(query);\n }\n\n /**\n * Get a single session by key\n */\n async getSession(key: string) {\n return this.sessionManager.getSession(key);\n }\n\n /**\n * Delete a session\n */\n async deleteSession(key: string): Promise<{ deleted: boolean }> {\n const result = await this.sessionManager.deleteSession(key);\n return { deleted: result };\n }\n\n /**\n * Delete multiple sessions\n */\n async deleteSessions(keys: string[]): Promise<{ success: string[]; failed: string[] }> {\n return this.sessionManager.deleteSessions(keys);\n }\n\n /**\n * Rename a session\n */\n async renameSession(key: string, name: string): Promise<{ renamed: boolean }> {\n await this.sessionManager.renameSession(key, name);\n return { renamed: true };\n }\n\n /**\n * Tag a session\n */\n async tagSession(key: string, tags: string[]): Promise<{ tagged: boolean }> {\n await this.sessionManager.tagSession(key, tags);\n return { tagged: true };\n }\n\n /**\n * Remove tags from a session\n */\n async untagSession(key: string, tags: string[]): Promise<{ untagged: boolean }> {\n await this.sessionManager.untagSession(key, tags);\n return { untagged: true };\n }\n\n /**\n * Archive a session\n */\n async archiveSession(key: string): Promise<{ archived: boolean }> {\n await this.sessionManager.archiveSession(key);\n return { archived: true };\n }\n\n /**\n * Unarchive a session\n */\n async unarchiveSession(key: string): Promise<{ unarchived: boolean }> {\n await this.sessionManager.unarchiveSession(key);\n return { unarchived: true };\n }\n\n /**\n * Pin a session\n */\n async pinSession(key: string): Promise<{ pinned: boolean }> {\n await this.sessionManager.pinSession(key);\n return { pinned: true };\n }\n\n /**\n * Unpin a session\n */\n async unpinSession(key: string): Promise<{ unpinned: boolean }> {\n await this.sessionManager.unpinSession(key);\n return { unpinned: true };\n }\n\n /**\n * Search sessions\n */\n async searchSessions(query: string) {\n return this.sessionManager.searchSessions(query);\n }\n\n /**\n * Search within a session\n */\n async searchInSession(key: string, keyword: string) {\n return this.sessionManager.searchInSession(key, keyword);\n }\n\n /**\n * Export a session\n */\n async exportSession(key: string, format: ExportFormat): Promise<{ content: string }> {\n const content = await this.sessionManager.exportSession(key, format);\n return { content };\n }\n\n /**\n * Get session statistics\n */\n async getSessionStats() {\n return this.sessionManager.getStats();\n }\n\n /**\n * Get unique chat IDs from sessions, grouped by channel\n * Returns a list of channel/chatId pairs for cron job configuration.\n * `chatId` is the session-store routing suffix (unique per bot account + peer).\n * When `routing` exists, `peerId` is the platform id (e.g. Telegram numeric chat id).\n */\n async getSessionChatIds(channel?: string): Promise<\n Array<{\n channel: string;\n chatId: string;\n lastActive: string;\n accountId?: string;\n peerKind?: string;\n peerId?: string;\n }>\n > {\n const result = await this.sessionManager.listSessions({\n limit: 1000,\n sortBy: 'lastAccessedAt',\n sortOrder: 'desc',\n ...(channel ? { channel } : {}),\n });\n\n // Group by channel:chatId to get unique pairs\n const seen = new Set<string>();\n const chatIds: Array<{\n channel: string;\n chatId: string;\n lastActive: string;\n accountId?: string;\n peerKind?: string;\n peerId?: string;\n }> = [];\n\n for (const session of result.items) {\n const key = `${session.sourceChannel}:${session.sourceChatId}`;\n if (!seen.has(key) && session.sourceChannel && session.sourceChatId) {\n seen.add(key);\n const r = session.routing;\n chatIds.push({\n channel: session.sourceChannel,\n chatId: session.sourceChatId,\n lastActive: session.lastAccessedAt,\n ...(r\n ? {\n accountId: r.accountId,\n peerKind: r.peerKind,\n peerId: r.peerId,\n }\n : {}),\n });\n }\n }\n\n return chatIds;\n }\n\n /**\n * Validate authentication token from request headers.\n * Returns true if auth is disabled (mode: 'none') or token is valid.\n */\n validateAuth(headers?: Record<string, string | string[] | undefined>): boolean {\n const token = extractToken(headers);\n return validateToken(this.auth, token);\n }\n\n /**\n * Get current auth mode.\n */\n getAuthMode(): 'none' | 'token' {\n return this.auth.mode;\n }\n\n /**\n * Get current auth token (for CLI server integration).\n * Returns undefined if mode is 'none'.\n */\n getAuthToken(): string | undefined {\n return this.auth.mode === 'token' ? this.auth.token : undefined;\n }\n\n /**\n * Refresh (regenerate) the gateway auth token.\n * Returns the new token.\n */\n async refreshAuthToken(): Promise<string> {\n if (this.auth.mode !== 'token') {\n throw new Error('Cannot refresh token: auth mode is not token');\n }\n\n // Generate new token\n const newToken = crypto.randomBytes(24).toString('hex');\n \n // Update in-memory auth\n this.auth.token = newToken;\n \n // Update config\n this.config = {\n ...this.config,\n gateway: {\n ...this.config.gateway,\n auth: {\n ...this.config.gateway?.auth,\n mode: 'token',\n token: newToken,\n },\n },\n };\n \n await this.writeConfigAndReloadFromDisk(this.config);\n\n log.info({ tokenPreview: `${newToken.slice(0, 8)}...` }, 'Gateway token refreshed');\n \n return newToken;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { init_agent_scope, resolveAgentBootstrapDir, resolveDefaultAgentId } from "../agent/agent-scope.js";
|
|
2
|
-
import { WORKSPACE_FILES, init_paths } from "../config/paths.js";
|
|
3
2
|
import { getWorkspacePath, init_schema } from "../config/schema.js";
|
|
3
|
+
import { WORKSPACE_FILES, init_paths } from "../config/paths.js";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
//#region src/gateway/workspace-heartbeat-path.ts
|
|
6
6
|
init_schema();
|
|
@@ -8,6 +8,8 @@ init_package_version();
|
|
|
8
8
|
const REGISTRY_BASE = "https://registry.npmjs.org";
|
|
9
9
|
const PACKAGE_NAME = "@xopcai/xopc";
|
|
10
10
|
const REGISTRY_TIMEOUT_MS = 3500;
|
|
11
|
+
const MAX_REGISTRY_RETRIES = 2;
|
|
12
|
+
const INITIAL_REGISTRY_RETRY_DELAY_MS = 500;
|
|
11
13
|
/**
|
|
12
14
|
* Fetch the version published under a specific npm dist-tag.
|
|
13
15
|
* Uses the abbreviated packument endpoint: `GET /<pkg>/<tag>`.
|
|
@@ -15,26 +17,38 @@ const REGISTRY_TIMEOUT_MS = 3500;
|
|
|
15
17
|
async function fetchNpmTagVersion(params) {
|
|
16
18
|
const timeoutMs = params.timeoutMs ?? REGISTRY_TIMEOUT_MS;
|
|
17
19
|
const url = `${REGISTRY_BASE}/${encodeURIComponent(PACKAGE_NAME).replace("%40", "@")}/${encodeURIComponent(params.tag)}`;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
20
|
+
let lastError;
|
|
21
|
+
for (let attempt = 0; attempt <= MAX_REGISTRY_RETRIES; attempt++) {
|
|
22
|
+
if (attempt > 0) {
|
|
23
|
+
const delayMs = INITIAL_REGISTRY_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
24
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
28
|
+
if (!response.ok) {
|
|
29
|
+
lastError = `HTTP ${response.status}`;
|
|
30
|
+
if (response.status >= 400 && response.status < 500) return {
|
|
31
|
+
tag: params.tag,
|
|
32
|
+
version: null,
|
|
33
|
+
error: lastError
|
|
34
|
+
};
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const json = await response.json();
|
|
38
|
+
const version = typeof json?.version === "string" ? json.version : null;
|
|
39
|
+
return {
|
|
40
|
+
tag: params.tag,
|
|
41
|
+
version
|
|
42
|
+
};
|
|
43
|
+
} catch (err) {
|
|
44
|
+
lastError = String(err);
|
|
45
|
+
}
|
|
37
46
|
}
|
|
47
|
+
return {
|
|
48
|
+
tag: params.tag,
|
|
49
|
+
version: null,
|
|
50
|
+
error: lastError
|
|
51
|
+
};
|
|
38
52
|
}
|
|
39
53
|
/**
|
|
40
54
|
* Resolve the best version for the given update channel.
|
|
@@ -105,8 +119,27 @@ function compareSemver(a, b) {
|
|
|
105
119
|
if (parsedA.prerelease === null && parsedB.prerelease === null) return 0;
|
|
106
120
|
if (parsedA.prerelease !== null && parsedB.prerelease === null) return -1;
|
|
107
121
|
if (parsedA.prerelease === null && parsedB.prerelease !== null) return 1;
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
return comparePrereleaseIdentifiers(parsedA.prerelease, parsedB.prerelease);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Compare prerelease strings per semver 2.0 §11.
|
|
126
|
+
*/
|
|
127
|
+
function comparePrereleaseIdentifiers(a, b) {
|
|
128
|
+
const partsA = a.split(".");
|
|
129
|
+
const partsB = b.split(".");
|
|
130
|
+
const length = Math.min(partsA.length, partsB.length);
|
|
131
|
+
for (let i = 0; i < length; i++) {
|
|
132
|
+
const segA = partsA[i];
|
|
133
|
+
const segB = partsB[i];
|
|
134
|
+
if (segA === segB) continue;
|
|
135
|
+
const numA = /^\d+$/.test(segA) ? parseInt(segA, 10) : null;
|
|
136
|
+
const numB = /^\d+$/.test(segB) ? parseInt(segB, 10) : null;
|
|
137
|
+
if (numA !== null && numB !== null) return numA < numB ? -1 : 1;
|
|
138
|
+
if (numA !== null && numB === null) return -1;
|
|
139
|
+
if (numA === null && numB !== null) return 1;
|
|
140
|
+
return segA < segB ? -1 : 1;
|
|
141
|
+
}
|
|
142
|
+
if (partsA.length !== partsB.length) return partsA.length < partsB.length ? -1 : 1;
|
|
110
143
|
return 0;
|
|
111
144
|
}
|
|
112
145
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-check.js","names":[],"sources":["../../../src/infra/update-check.ts"],"sourcesContent":["// src/infra/update-check.ts\n\nimport { access, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { PACKAGE_VERSION } from '../package-version.js';\n\nimport { channelToNpmTag, type UpdateChannel } from './update-channels.js';\n\n// --- Types ---\n\nexport type InstallKind = 'git' | 'package' | 'unknown';\n\nexport type NpmTagResult = {\n tag: string;\n version: string | null;\n error?: string;\n};\n\nexport type UpdateCheckResult = {\n installKind: InstallKind;\n root: string | null;\n};\n\nexport type UpdateAvailable = {\n currentVersion: string;\n latestVersion: string;\n channel: string;\n};\n\n// --- npm Registry ---\n\nconst REGISTRY_BASE = 'https://registry.npmjs.org';\nconst PACKAGE_NAME = '@xopcai/xopc';\nconst REGISTRY_TIMEOUT_MS = 3500;\n\n/**\n * Fetch the version published under a specific npm dist-tag.\n * Uses the abbreviated packument endpoint: `GET /<pkg>/<tag>`.\n */\nexport async function fetchNpmTagVersion(params: {\n tag: string;\n timeoutMs?: number;\n}): Promise<NpmTagResult> {\n const timeoutMs = params.timeoutMs ?? REGISTRY_TIMEOUT_MS;\n const encodedName = encodeURIComponent(PACKAGE_NAME).replace('%40', '@');\n const url = `${REGISTRY_BASE}/${encodedName}/${encodeURIComponent(params.tag)}`;\n try {\n const response = await fetch(url, {\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (!response.ok) {\n return { tag: params.tag, version: null, error: `HTTP ${response.status}` };\n }\n const json = (await response.json()) as { version?: unknown };\n const version = typeof json?.version === 'string' ? json.version : null;\n return { tag: params.tag, version };\n } catch (err) {\n return { tag: params.tag, version: null, error: String(err) };\n }\n}\n\n/**\n * Resolve the best version for the given update channel.\n * For beta channel: if the beta tag version is older than latest, return latest instead.\n */\nexport async function resolveNpmChannelTag(params: {\n channel: UpdateChannel;\n timeoutMs?: number;\n}): Promise<{ tag: string; version: string | null }> {\n const channelTag = channelToNpmTag(params.channel);\n const channelResult = await fetchNpmTagVersion({ tag: channelTag, timeoutMs: params.timeoutMs });\n\n if (params.channel !== 'beta') {\n return { tag: channelTag, version: channelResult.version };\n }\n\n // For beta: also check latest, return whichever is newer\n const latestResult = await fetchNpmTagVersion({ tag: 'latest', timeoutMs: params.timeoutMs });\n if (!latestResult.version) {\n return { tag: channelTag, version: channelResult.version };\n }\n if (!channelResult.version) {\n return { tag: 'latest', version: latestResult.version };\n }\n const comparison = compareSemver(channelResult.version, latestResult.version);\n if (comparison !== null && comparison < 0) {\n return { tag: 'latest', version: latestResult.version };\n }\n return { tag: channelTag, version: channelResult.version };\n}\n\n// --- Semver comparison ---\n\n/**\n * Parse a version string into comparable numeric parts.\n * Handles formats like \"1.2.3\", \"1.2.3-beta.1\".\n * Returns null for unparseable strings.\n */\nfunction parseSemverParts(\n version: string,\n): { major: number; minor: number; patch: number; prerelease: string | null } | null {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-(.+))?$/.exec(version.trim());\n if (!match) return null;\n return {\n major: parseInt(match[1], 10),\n minor: parseInt(match[2], 10),\n patch: parseInt(match[3], 10),\n prerelease: match[4] ?? null,\n };\n}\n\n/**\n * Compare two semver strings. Returns:\n * -1 if a < b, 0 if equal, 1 if a > b, null if either is unparseable.\n * Prerelease versions are considered older than the same version without prerelease.\n */\nexport function compareSemver(a: string | null, b: string | null): number | null {\n if (!a || !b) return null;\n const parsedA = parseSemverParts(a);\n const parsedB = parseSemverParts(b);\n if (!parsedA || !parsedB) return null;\n\n for (const field of ['major', 'minor', 'patch'] as const) {\n if (parsedA[field] !== parsedB[field]) {\n return parsedA[field] < parsedB[field] ? -1 : 1;\n }\n }\n\n // Both have same major.minor.patch — compare prerelease\n if (parsedA.prerelease === null && parsedB.prerelease === null) return 0;\n if (parsedA.prerelease !== null && parsedB.prerelease === null) return -1; // pre < release\n if (parsedA.prerelease === null && parsedB.prerelease !== null) return 1;\n\n // Both have prerelease — lexicographic fallback\n if (parsedA.prerelease! < parsedB.prerelease!) return -1;\n if (parsedA.prerelease! > parsedB.prerelease!) return 1;\n return 0;\n}\n\n// --- Install kind detection ---\n\n/**\n * Detect whether the current process is running from a git checkout or npm-installed package.\n * Checks for `.git` directory in the package root.\n */\nexport async function detectInstallKind(packageRoot: string): Promise<InstallKind> {\n try {\n await access(join(packageRoot, '.git'));\n return 'git';\n } catch {\n // No .git directory — likely installed via npm\n try {\n await access(join(packageRoot, 'package.json'));\n return 'package';\n } catch {\n return 'unknown';\n }\n }\n}\n\n/**\n * Resolve the xopc package root directory.\n * Walks up from this file to find package.json with name @xopcai/xopc.\n */\nexport async function resolvePackageRoot(): Promise<string | null> {\n let current = dirname(fileURLToPath(import.meta.url));\n for (let depth = 0; depth < 20; depth++) {\n const pkgPath = join(current, 'package.json');\n try {\n const raw = await readFile(pkgPath, 'utf-8');\n const parsed = JSON.parse(raw) as { name?: unknown };\n if (typeof parsed.name === 'string' && parsed.name === PACKAGE_NAME) {\n return current;\n }\n } catch {\n // continue\n }\n const parent = dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n return null;\n}\n\n/** Get the current running version from package.json. */\nexport function getCurrentVersion(): string {\n return PACKAGE_VERSION;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"update-check.js","names":[],"sources":["../../../src/infra/update-check.ts"],"sourcesContent":["// src/infra/update-check.ts\n\nimport { access, readFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nimport { PACKAGE_VERSION } from '../package-version.js';\n\nimport { channelToNpmTag, type UpdateChannel } from './update-channels.js';\n\n// --- Types ---\n\nexport type InstallKind = 'git' | 'package' | 'unknown';\n\nexport type NpmTagResult = {\n tag: string;\n version: string | null;\n error?: string;\n};\n\nexport type UpdateCheckResult = {\n installKind: InstallKind;\n root: string | null;\n};\n\nexport type UpdateAvailable = {\n currentVersion: string;\n latestVersion: string;\n channel: string;\n};\n\n// --- npm Registry ---\n\nconst REGISTRY_BASE = 'https://registry.npmjs.org';\nconst PACKAGE_NAME = '@xopcai/xopc';\nconst REGISTRY_TIMEOUT_MS = 3500;\nconst MAX_REGISTRY_RETRIES = 2;\nconst INITIAL_REGISTRY_RETRY_DELAY_MS = 500;\n\n/**\n * Fetch the version published under a specific npm dist-tag.\n * Uses the abbreviated packument endpoint: `GET /<pkg>/<tag>`.\n */\nexport async function fetchNpmTagVersion(params: {\n tag: string;\n timeoutMs?: number;\n}): Promise<NpmTagResult> {\n const timeoutMs = params.timeoutMs ?? REGISTRY_TIMEOUT_MS;\n const encodedName = encodeURIComponent(PACKAGE_NAME).replace('%40', '@');\n const url = `${REGISTRY_BASE}/${encodedName}/${encodeURIComponent(params.tag)}`;\n\n let lastError: string | undefined;\n\n for (let attempt = 0; attempt <= MAX_REGISTRY_RETRIES; attempt++) {\n if (attempt > 0) {\n const delayMs = INITIAL_REGISTRY_RETRY_DELAY_MS * Math.pow(2, attempt - 1);\n await new Promise((r) => setTimeout(r, delayMs));\n }\n\n try {\n const response = await fetch(url, {\n signal: AbortSignal.timeout(timeoutMs),\n });\n if (!response.ok) {\n lastError = `HTTP ${response.status}`;\n if (response.status >= 400 && response.status < 500) {\n return { tag: params.tag, version: null, error: lastError };\n }\n continue;\n }\n const json = (await response.json()) as { version?: unknown };\n const version = typeof json?.version === 'string' ? json.version : null;\n return { tag: params.tag, version };\n } catch (err) {\n lastError = String(err);\n }\n }\n\n return { tag: params.tag, version: null, error: lastError };\n}\n\n/**\n * Resolve the best version for the given update channel.\n * For beta channel: if the beta tag version is older than latest, return latest instead.\n */\nexport async function resolveNpmChannelTag(params: {\n channel: UpdateChannel;\n timeoutMs?: number;\n}): Promise<{ tag: string; version: string | null }> {\n const channelTag = channelToNpmTag(params.channel);\n const channelResult = await fetchNpmTagVersion({ tag: channelTag, timeoutMs: params.timeoutMs });\n\n if (params.channel !== 'beta') {\n return { tag: channelTag, version: channelResult.version };\n }\n\n // For beta: also check latest, return whichever is newer\n const latestResult = await fetchNpmTagVersion({ tag: 'latest', timeoutMs: params.timeoutMs });\n if (!latestResult.version) {\n return { tag: channelTag, version: channelResult.version };\n }\n if (!channelResult.version) {\n return { tag: 'latest', version: latestResult.version };\n }\n const comparison = compareSemver(channelResult.version, latestResult.version);\n if (comparison !== null && comparison < 0) {\n return { tag: 'latest', version: latestResult.version };\n }\n return { tag: channelTag, version: channelResult.version };\n}\n\n// --- Semver comparison ---\n\n/**\n * Parse a version string into comparable numeric parts.\n * Handles formats like \"1.2.3\", \"1.2.3-beta.1\".\n * Returns null for unparseable strings.\n */\nfunction parseSemverParts(\n version: string,\n): { major: number; minor: number; patch: number; prerelease: string | null } | null {\n const match = /^v?(\\d+)\\.(\\d+)\\.(\\d+)(?:-(.+))?$/.exec(version.trim());\n if (!match) return null;\n return {\n major: parseInt(match[1], 10),\n minor: parseInt(match[2], 10),\n patch: parseInt(match[3], 10),\n prerelease: match[4] ?? null,\n };\n}\n\n/**\n * Compare two semver strings. Returns:\n * -1 if a < b, 0 if equal, 1 if a > b, null if either is unparseable.\n * Prerelease versions are considered older than the same version without prerelease.\n */\nexport function compareSemver(a: string | null, b: string | null): number | null {\n if (!a || !b) return null;\n const parsedA = parseSemverParts(a);\n const parsedB = parseSemverParts(b);\n if (!parsedA || !parsedB) return null;\n\n for (const field of ['major', 'minor', 'patch'] as const) {\n if (parsedA[field] !== parsedB[field]) {\n return parsedA[field] < parsedB[field] ? -1 : 1;\n }\n }\n\n // Both have same major.minor.patch — compare prerelease\n if (parsedA.prerelease === null && parsedB.prerelease === null) return 0;\n if (parsedA.prerelease !== null && parsedB.prerelease === null) return -1; // pre < release\n if (parsedA.prerelease === null && parsedB.prerelease !== null) return 1;\n\n return comparePrereleaseIdentifiers(parsedA.prerelease!, parsedB.prerelease!);\n}\n\n/**\n * Compare prerelease strings per semver 2.0 §11.\n */\nfunction comparePrereleaseIdentifiers(a: string, b: string): number {\n const partsA = a.split('.');\n const partsB = b.split('.');\n const length = Math.min(partsA.length, partsB.length);\n\n for (let i = 0; i < length; i++) {\n const segA = partsA[i]!;\n const segB = partsB[i]!;\n if (segA === segB) continue;\n\n const numA = /^\\d+$/.test(segA) ? parseInt(segA, 10) : null;\n const numB = /^\\d+$/.test(segB) ? parseInt(segB, 10) : null;\n\n if (numA !== null && numB !== null) {\n return numA < numB ? -1 : 1;\n }\n if (numA !== null && numB === null) return -1;\n if (numA === null && numB !== null) return 1;\n return segA < segB ? -1 : 1;\n }\n\n if (partsA.length !== partsB.length) {\n return partsA.length < partsB.length ? -1 : 1;\n }\n return 0;\n}\n\n// --- Install kind detection ---\n\n/**\n * Detect whether the current process is running from a git checkout or npm-installed package.\n * Checks for `.git` directory in the package root.\n */\nexport async function detectInstallKind(packageRoot: string): Promise<InstallKind> {\n try {\n await access(join(packageRoot, '.git'));\n return 'git';\n } catch {\n // No .git directory — likely installed via npm\n try {\n await access(join(packageRoot, 'package.json'));\n return 'package';\n } catch {\n return 'unknown';\n }\n }\n}\n\n/**\n * Resolve the xopc package root directory.\n * Walks up from this file to find package.json with name @xopcai/xopc.\n */\nexport async function resolvePackageRoot(): Promise<string | null> {\n let current = dirname(fileURLToPath(import.meta.url));\n for (let depth = 0; depth < 20; depth++) {\n const pkgPath = join(current, 'package.json');\n try {\n const raw = await readFile(pkgPath, 'utf-8');\n const parsed = JSON.parse(raw) as { name?: unknown };\n if (typeof parsed.name === 'string' && parsed.name === PACKAGE_NAME) {\n return current;\n }\n } catch {\n // continue\n }\n const parent = dirname(current);\n if (parent === current) return null;\n current = parent;\n }\n return null;\n}\n\n/** Get the current running version from package.json. */\nexport function getCurrentVersion(): string {\n return PACKAGE_VERSION;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
type LockInfo = {
|
|
2
|
+
pid: number;
|
|
3
|
+
startedAt: string;
|
|
4
|
+
source: 'gateway' | 'cli' | 'auto';
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Attempt to acquire the update lock. Returns a release function on success, or null if
|
|
8
|
+
* another live process holds the lock.
|
|
9
|
+
*/
|
|
10
|
+
export declare function acquireUpdateLock(source: LockInfo['source']): Promise<{
|
|
11
|
+
release: () => Promise<void>;
|
|
12
|
+
} | null>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { init_paths_state, resolveUpdateLockPath } from "../config/paths-state.js";
|
|
2
|
+
import { createLogger } from "../utils/logger/index.js";
|
|
3
|
+
import { init_logger } from "../utils/logger.js";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
6
|
+
//#region src/infra/update-lock.ts
|
|
7
|
+
init_paths_state();
|
|
8
|
+
init_logger();
|
|
9
|
+
const log = createLogger("UpdateLock");
|
|
10
|
+
const STALE_THRESHOLD_MS = 3600 * 1e3;
|
|
11
|
+
function resolveLockPath() {
|
|
12
|
+
return resolveUpdateLockPath();
|
|
13
|
+
}
|
|
14
|
+
function isProcessAlive(pid) {
|
|
15
|
+
try {
|
|
16
|
+
process.kill(pid, 0);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Attempt to acquire the update lock. Returns a release function on success, or null if
|
|
24
|
+
* another live process holds the lock.
|
|
25
|
+
*/
|
|
26
|
+
async function acquireUpdateLock(source) {
|
|
27
|
+
const lockPath = resolveLockPath();
|
|
28
|
+
try {
|
|
29
|
+
const raw = await readFile(lockPath, "utf-8");
|
|
30
|
+
const existing = JSON.parse(raw);
|
|
31
|
+
const startedAt = Date.parse(existing.startedAt);
|
|
32
|
+
const age = Date.now() - startedAt;
|
|
33
|
+
if (isProcessAlive(existing.pid) && age < STALE_THRESHOLD_MS) {
|
|
34
|
+
log.info({
|
|
35
|
+
existingPid: existing.pid,
|
|
36
|
+
source: existing.source,
|
|
37
|
+
ageMs: age
|
|
38
|
+
}, "Update lock held by another live process");
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
log.warn({
|
|
42
|
+
existingPid: existing.pid,
|
|
43
|
+
source: existing.source,
|
|
44
|
+
ageMs: age
|
|
45
|
+
}, "Reclaiming stale update lock");
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err.code !== "ENOENT") log.warn({ err }, "Failed to read update lock; proceeding");
|
|
48
|
+
}
|
|
49
|
+
const info = {
|
|
50
|
+
pid: process.pid,
|
|
51
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
52
|
+
source
|
|
53
|
+
};
|
|
54
|
+
await mkdir(dirname(lockPath), { recursive: true });
|
|
55
|
+
await writeFile(lockPath, JSON.stringify(info, null, 2), "utf-8");
|
|
56
|
+
const release = async () => {
|
|
57
|
+
try {
|
|
58
|
+
const raw = await readFile(lockPath, "utf-8");
|
|
59
|
+
if (JSON.parse(raw).pid === process.pid) await unlink(lockPath);
|
|
60
|
+
} catch {}
|
|
61
|
+
};
|
|
62
|
+
return { release };
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { acquireUpdateLock };
|
|
66
|
+
|
|
67
|
+
//# sourceMappingURL=update-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update-lock.js","names":[],"sources":["../../../src/infra/update-lock.ts"],"sourcesContent":["// src/infra/update-lock.ts\n\nimport { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\nimport { resolveUpdateLockPath } from '../config/paths-state.js';\nimport { createLogger } from '../utils/logger.js';\n\nconst log = createLogger('UpdateLock');\n\nconst STALE_THRESHOLD_MS = 60 * 60 * 1000; // 1 hour\n\ntype LockInfo = {\n pid: number;\n startedAt: string;\n source: 'gateway' | 'cli' | 'auto';\n};\n\nfunction resolveLockPath(): string {\n return resolveUpdateLockPath();\n}\n\nfunction isProcessAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Attempt to acquire the update lock. Returns a release function on success, or null if\n * another live process holds the lock.\n */\nexport async function acquireUpdateLock(\n source: LockInfo['source'],\n): Promise<{ release: () => Promise<void> } | null> {\n const lockPath = resolveLockPath();\n\n try {\n const raw = await readFile(lockPath, 'utf-8');\n const existing = JSON.parse(raw) as LockInfo;\n const startedAt = Date.parse(existing.startedAt);\n const age = Date.now() - startedAt;\n\n if (isProcessAlive(existing.pid) && age < STALE_THRESHOLD_MS) {\n log.info(\n { existingPid: existing.pid, source: existing.source, ageMs: age },\n 'Update lock held by another live process',\n );\n return null;\n }\n\n log.warn(\n { existingPid: existing.pid, source: existing.source, ageMs: age },\n 'Reclaiming stale update lock',\n );\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'ENOENT') {\n log.warn({ err }, 'Failed to read update lock; proceeding');\n }\n }\n\n const info: LockInfo = {\n pid: process.pid,\n startedAt: new Date().toISOString(),\n source,\n };\n\n await mkdir(dirname(lockPath), { recursive: true });\n await writeFile(lockPath, JSON.stringify(info, null, 2), 'utf-8');\n\n const release = async () => {\n try {\n const raw = await readFile(lockPath, 'utf-8');\n const current = JSON.parse(raw) as LockInfo;\n if (current.pid === process.pid) {\n await unlink(lockPath);\n }\n } catch {\n // Lock already released or stolen — fine\n }\n };\n\n return { release };\n}\n"],"mappings":";;;;;;kBAKiE;aACf;AAElD,MAAM,MAAM,aAAa,aAAa;AAEtC,MAAM,qBAAqB,OAAU;AAQrC,SAAS,kBAA0B;AACjC,QAAO,uBAAuB;;AAGhC,SAAS,eAAe,KAAsB;AAC5C,KAAI;AACF,UAAQ,KAAK,KAAK,EAAE;AACpB,SAAO;SACD;AACN,SAAO;;;;;;;AAQX,eAAsB,kBACpB,QACkD;CAClD,MAAM,WAAW,iBAAiB;AAElC,KAAI;EACF,MAAM,MAAM,MAAM,SAAS,UAAU,QAAQ;EAC7C,MAAM,WAAW,KAAK,MAAM,IAAI;EAChC,MAAM,YAAY,KAAK,MAAM,SAAS,UAAU;EAChD,MAAM,MAAM,KAAK,KAAK,GAAG;AAEzB,MAAI,eAAe,SAAS,IAAI,IAAI,MAAM,oBAAoB;AAC5D,OAAI,KACF;IAAE,aAAa,SAAS;IAAK,QAAQ,SAAS;IAAQ,OAAO;IAAK,EAClE,2CACD;AACD,UAAO;;AAGT,MAAI,KACF;GAAE,aAAa,SAAS;GAAK,QAAQ,SAAS;GAAQ,OAAO;GAAK,EAClE,+BACD;UACM,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,KAAI,KAAK,EAAE,KAAK,EAAE,yCAAyC;;CAI/D,MAAM,OAAiB;EACrB,KAAK,QAAQ;EACb,4BAAW,IAAI,MAAM,EAAC,aAAa;EACnC;EACD;AAED,OAAM,MAAM,QAAQ,SAAS,EAAE,EAAE,WAAW,MAAM,CAAC;AACnD,OAAM,UAAU,UAAU,KAAK,UAAU,MAAM,MAAM,EAAE,EAAE,QAAQ;CAEjE,MAAM,UAAU,YAAY;AAC1B,MAAI;GACF,MAAM,MAAM,MAAM,SAAS,UAAU,QAAQ;AAE7C,OADgB,KAAK,MAAM,IAChB,CAAC,QAAQ,QAAQ,IAC1B,OAAM,OAAO,SAAS;UAElB;;AAKV,QAAO,EAAE,SAAS"}
|
|
@@ -6,13 +6,14 @@ export type AutoUpdateResult = {
|
|
|
6
6
|
stdout?: string;
|
|
7
7
|
stderr?: string;
|
|
8
8
|
};
|
|
9
|
-
/**
|
|
10
|
-
* Spawn a child process to execute `xopc update --yes --channel <channel> --json`.
|
|
11
|
-
* Uses the current runtime (process.execPath) and entry point (process.argv[1]) to
|
|
12
|
-
* ensure the correct Node.js version and binary path are used.
|
|
13
|
-
*/
|
|
14
9
|
export declare function runAutoUpdateCommand(params: {
|
|
15
10
|
channel: UpdateChannel;
|
|
16
11
|
root?: string | null;
|
|
17
12
|
timeoutMs?: number;
|
|
18
13
|
}): Promise<AutoUpdateResult>;
|
|
14
|
+
export declare function runAutoUpdateCommandWithProgress(params: {
|
|
15
|
+
channel: UpdateChannel;
|
|
16
|
+
root?: string | null;
|
|
17
|
+
timeoutMs?: number;
|
|
18
|
+
onProgress?: (line: string, source: 'stdout' | 'stderr') => void | Promise<void>;
|
|
19
|
+
}): Promise<AutoUpdateResult>;
|