@nextclaw/server 0.12.5 → 0.12.7

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/index.d.ts CHANGED
@@ -349,6 +349,37 @@ declare class UiAuthService {
349
349
  };
350
350
  }
351
351
  //#endregion
352
+ //#region src/ui/runtime-control.types.d.ts
353
+ type RuntimeControlEnvironment = "desktop-embedded" | "managed-local-service" | "self-hosted-web" | "shared-web";
354
+ type RuntimeLifecycleState = "healthy" | "starting-service" | "restarting-service" | "stopping-service" | "restarting-app" | "recovering" | "unavailable" | "failed";
355
+ type RuntimeActionImpact = "none" | "brief-ui-disconnect" | "full-app-relaunch";
356
+ type RuntimeActionCapability = {
357
+ available: boolean;
358
+ requiresConfirmation: boolean;
359
+ impact: RuntimeActionImpact;
360
+ reasonIfUnavailable?: string;
361
+ };
362
+ type RuntimeServiceState = "running" | "stopped" | "starting" | "stopping" | "restarting" | "unknown";
363
+ type RuntimeControlView = {
364
+ environment: RuntimeControlEnvironment;
365
+ lifecycle: RuntimeLifecycleState;
366
+ serviceState: RuntimeServiceState;
367
+ canStartService: RuntimeActionCapability;
368
+ canRestartService: RuntimeActionCapability;
369
+ canStopService: RuntimeActionCapability;
370
+ canRestartApp: RuntimeActionCapability;
371
+ ownerLabel?: string;
372
+ managementHint?: string;
373
+ message?: string;
374
+ };
375
+ type RuntimeControlAction = "start-service" | "restart-service" | "stop-service" | "restart-app";
376
+ type RuntimeControlActionResult = {
377
+ accepted: boolean;
378
+ action: RuntimeControlAction;
379
+ lifecycle: RuntimeLifecycleState;
380
+ message: string;
381
+ };
382
+ //#endregion
352
383
  //#region src/ui/ui-routes/types.d.ts
353
384
  type UiRouterOptions = {
354
385
  configPath: string;
@@ -362,6 +393,7 @@ type UiRouterOptions = {
362
393
  ncpSessionService?: UiNcpSessionService;
363
394
  authService?: UiAuthService;
364
395
  remoteAccess?: UiRemoteAccessHost;
396
+ runtimeControl?: UiRuntimeControlHost;
365
397
  getBootstrapStatus?: () => BootstrapStatusView;
366
398
  getPluginChannelBindings?: () => PluginChannelBinding[];
367
399
  getPluginUiMetadata?: () => PluginUiMetadata[];
@@ -377,6 +409,12 @@ type UiRemoteAccessHost = {
377
409
  runDoctor: () => Promise<RemoteDoctorView>;
378
410
  controlService: (action: RemoteServiceAction) => Promise<RemoteServiceActionResult>;
379
411
  };
412
+ type UiRuntimeControlHost = {
413
+ getControl: () => Promise<RuntimeControlView> | RuntimeControlView;
414
+ startService: () => Promise<RuntimeControlActionResult> | RuntimeControlActionResult;
415
+ restartService: () => Promise<RuntimeControlActionResult> | RuntimeControlActionResult;
416
+ stopService: () => Promise<RuntimeControlActionResult> | RuntimeControlActionResult;
417
+ };
380
418
  //#endregion
381
419
  //#region src/ui/chat-session-type.types.d.ts
382
420
  type ChatSessionTypeCtaView = {
@@ -760,6 +798,12 @@ type AgentBindingView = {
760
798
  type SessionConfigView = {
761
799
  dmScope?: "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer";
762
800
  };
801
+ type RuntimeEntryView = {
802
+ enabled?: boolean;
803
+ label?: string;
804
+ type: string;
805
+ config?: Record<string, unknown>;
806
+ };
763
807
  type SessionEntryView = {
764
808
  key: string;
765
809
  createdAt: string;
@@ -808,6 +852,7 @@ type SessionPatchUpdate = {
808
852
  preferredThinking?: ThinkingLevel | null;
809
853
  sessionType?: string | null;
810
854
  projectRoot?: string | null;
855
+ uiReadAt?: string | null;
811
856
  clearHistory?: boolean;
812
857
  };
813
858
  type SessionSkillEntryView = {
@@ -843,6 +888,15 @@ type ServerPathBrowseView = {
843
888
  breadcrumbs: ServerPathBreadcrumbView[];
844
889
  entries: ServerPathEntryView[];
845
890
  };
891
+ type ServerPathReadView = {
892
+ requestedPath: string;
893
+ resolvedPath: string;
894
+ kind: "text" | "markdown" | "binary";
895
+ sizeBytes: number;
896
+ truncated: boolean;
897
+ text?: string;
898
+ languageHint?: string | null;
899
+ };
846
900
  type CronScheduleView = {
847
901
  kind: "at";
848
902
  atMs?: number | null;
@@ -913,6 +967,9 @@ type RuntimeConfigUpdate = {
913
967
  engine?: string;
914
968
  engineConfig?: Record<string, unknown>;
915
969
  };
970
+ runtimes?: {
971
+ entries?: Record<string, RuntimeEntryView> | null;
972
+ };
916
973
  list?: AgentProfileView[];
917
974
  };
918
975
  bindings?: AgentBindingView[];
@@ -1001,6 +1058,9 @@ type ConfigView = {
1001
1058
  contextTokens?: number;
1002
1059
  maxToolIterations?: number;
1003
1060
  };
1061
+ runtimes?: {
1062
+ entries?: Record<string, RuntimeEntryView>;
1063
+ };
1004
1064
  list?: AgentProfileView[];
1005
1065
  context?: {
1006
1066
  bootstrap?: {
@@ -1208,6 +1268,7 @@ type UiServerOptions = {
1208
1268
  ncpAgent?: UiNcpAgent;
1209
1269
  ncpSessionService?: UiNcpSessionService;
1210
1270
  remoteAccess?: UiRemoteAccessHost;
1271
+ runtimeControl?: UiRuntimeControlHost;
1211
1272
  getBootstrapStatus?: () => BootstrapStatusView;
1212
1273
  getPluginChannelBindings?: () => PluginChannelBinding[];
1213
1274
  getPluginUiMetadata?: () => PluginUiMetadata[];
@@ -1287,4 +1348,4 @@ declare function getUiBridgeSecretPath(): string;
1287
1348
  declare function readUiBridgeSecret(): string | null;
1288
1349
  declare function ensureUiBridgeSecret(): string;
1289
1350
  //#endregion
1290
- export { AgentBindingView, AgentCreateRequest, AgentDeleteResult, AgentProfileView, AgentUpdateRequest, ApiError, ApiResponse, AppMetaView, AuthEnabledUpdateRequest, AuthLoginRequest, AuthPasswordUpdateRequest, AuthSetupRequest, AuthStatusView, BindingPeerView, BochaFreshnessValue, BootstrapPhase, BootstrapRemoteState, BootstrapStageState, BootstrapStatusView, ChannelAuthPollRequest, ChannelAuthPollResult, ChannelAuthStartRequest, ChannelAuthStartResult, ChannelSpecView, type ChatSessionTypeCtaView, type ChatSessionTypeOptionView, type ChatSessionTypesView, ConfigActionExecuteRequest, ConfigActionExecuteResult, ConfigActionManifest, ConfigActionType, ConfigMetaView, ConfigSchemaResponse, ConfigUiHint, ConfigUiHints, ConfigView, CronActionResult, CronCreateRequest, CronCreateResult, CronEnableRequest, CronJobStateView, CronJobView, CronListView, CronPayloadView, CronRunRequest, CronScheduleView, DEFAULT_SESSION_TYPE, MarketplaceApiConfig, MarketplaceInstallKind, MarketplaceInstallSkillParams, MarketplaceInstallSpec, MarketplaceInstalledRecord, MarketplaceInstalledView, MarketplaceInstaller, MarketplaceItemSummary, MarketplaceItemType, MarketplaceItemView, MarketplaceListView, MarketplaceLocalizedTextMap, MarketplaceMcpContentView, MarketplaceMcpDoctorResult, MarketplaceMcpInstallKind, MarketplaceMcpInstallRequest, MarketplaceMcpInstallResult, MarketplaceMcpInstallSpec, MarketplaceMcpManageAction, MarketplaceMcpManageRequest, MarketplaceMcpManageResult, MarketplaceMcpTemplateInput, MarketplacePluginContentView, MarketplacePluginInstallKind, MarketplacePluginInstallRequest, MarketplacePluginInstallResult, MarketplacePluginManageAction, MarketplacePluginManageRequest, MarketplacePluginManageResult, MarketplaceRecommendationView, MarketplaceSkillContentView, MarketplaceSkillInstallKind, MarketplaceSkillInstallRequest, MarketplaceSkillInstallResult, MarketplaceSkillManageAction, MarketplaceSkillManageRequest, MarketplaceSkillManageResult, MarketplaceSort, NcpSessionSkillsView, ProviderAuthImportResult, ProviderAuthPollRequest, ProviderAuthPollResult, ProviderAuthStartRequest, ProviderAuthStartResult, ProviderConfigUpdate, ProviderConfigView, ProviderConnectionTestRequest, ProviderConnectionTestResult, ProviderCreateRequest, ProviderCreateResult, ProviderDeleteResult, ProviderSpecView, RemoteAccessView, RemoteAccountProfileUpdateRequest, RemoteAccountView, RemoteBrowserAuthPollRequest, RemoteBrowserAuthPollResult, RemoteBrowserAuthStartRequest, RemoteBrowserAuthStartResult, RemoteDoctorCheckView, RemoteDoctorView, RemoteLoginRequest, RemoteRuntimeView, RemoteServiceAction, RemoteServiceActionResult, RemoteServiceView, RemoteSettingsUpdateRequest, RemoteSettingsView, RuntimeConfigUpdate, SearchConfigUpdate, SearchConfigView, SearchProviderConfigView, SearchProviderName, SearchProviderSpecView, SecretProviderEnvView, SecretProviderExecView, SecretProviderFileView, SecretProviderView, SecretRefView, SecretSourceView, SecretsConfigUpdate, SecretsView, ServerPathBreadcrumbView, ServerPathBrowseView, ServerPathEntryView, SessionConfigView, SessionEntryView, SessionEventView, SessionHistoryView, SessionMessageView, SessionPatchUpdate, SessionPatchValidationError, SessionSkillEntryView, SessionTypeDescribeParams, SessionsListView, TavilySearchDepthValue, UiNcpAgent, UiNcpAssetPutView, UiNcpAssetView, UiNcpSessionListView, UiNcpSessionMessagesView, UiNcpSessionService, UiNcpStoredAssetRecord, type UiRemoteAccessHost, UiServerEvent, UiServerHandle, UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, ensureUiBridgeSecret, executeConfigAction, getSessionHistory, getUiBridgeSecretPath, listSessions, loadConfigOrDefault, patchSession, readUiBridgeSecret, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSearch, updateSecrets };
1351
+ export { AgentBindingView, AgentCreateRequest, AgentDeleteResult, AgentProfileView, AgentUpdateRequest, ApiError, ApiResponse, AppMetaView, AuthEnabledUpdateRequest, AuthLoginRequest, AuthPasswordUpdateRequest, AuthSetupRequest, AuthStatusView, BindingPeerView, BochaFreshnessValue, BootstrapPhase, BootstrapRemoteState, BootstrapStageState, BootstrapStatusView, ChannelAuthPollRequest, ChannelAuthPollResult, ChannelAuthStartRequest, ChannelAuthStartResult, ChannelSpecView, type ChatSessionTypeCtaView, type ChatSessionTypeOptionView, type ChatSessionTypesView, ConfigActionExecuteRequest, ConfigActionExecuteResult, ConfigActionManifest, ConfigActionType, ConfigMetaView, ConfigSchemaResponse, ConfigUiHint, ConfigUiHints, ConfigView, CronActionResult, CronCreateRequest, CronCreateResult, CronEnableRequest, CronJobStateView, CronJobView, CronListView, CronPayloadView, CronRunRequest, CronScheduleView, DEFAULT_SESSION_TYPE, MarketplaceApiConfig, MarketplaceInstallKind, MarketplaceInstallSkillParams, MarketplaceInstallSpec, MarketplaceInstalledRecord, MarketplaceInstalledView, MarketplaceInstaller, MarketplaceItemSummary, MarketplaceItemType, MarketplaceItemView, MarketplaceListView, MarketplaceLocalizedTextMap, MarketplaceMcpContentView, MarketplaceMcpDoctorResult, MarketplaceMcpInstallKind, MarketplaceMcpInstallRequest, MarketplaceMcpInstallResult, MarketplaceMcpInstallSpec, MarketplaceMcpManageAction, MarketplaceMcpManageRequest, MarketplaceMcpManageResult, MarketplaceMcpTemplateInput, MarketplacePluginContentView, MarketplacePluginInstallKind, MarketplacePluginInstallRequest, MarketplacePluginInstallResult, MarketplacePluginManageAction, MarketplacePluginManageRequest, MarketplacePluginManageResult, MarketplaceRecommendationView, MarketplaceSkillContentView, MarketplaceSkillInstallKind, MarketplaceSkillInstallRequest, MarketplaceSkillInstallResult, MarketplaceSkillManageAction, MarketplaceSkillManageRequest, MarketplaceSkillManageResult, MarketplaceSort, NcpSessionSkillsView, ProviderAuthImportResult, ProviderAuthPollRequest, ProviderAuthPollResult, ProviderAuthStartRequest, ProviderAuthStartResult, ProviderConfigUpdate, ProviderConfigView, ProviderConnectionTestRequest, ProviderConnectionTestResult, ProviderCreateRequest, ProviderCreateResult, ProviderDeleteResult, ProviderSpecView, RemoteAccessView, RemoteAccountProfileUpdateRequest, RemoteAccountView, RemoteBrowserAuthPollRequest, RemoteBrowserAuthPollResult, RemoteBrowserAuthStartRequest, RemoteBrowserAuthStartResult, RemoteDoctorCheckView, RemoteDoctorView, RemoteLoginRequest, RemoteRuntimeView, RemoteServiceAction, RemoteServiceActionResult, RemoteServiceView, RemoteSettingsUpdateRequest, RemoteSettingsView, RuntimeActionCapability, RuntimeActionImpact, RuntimeConfigUpdate, RuntimeControlAction, RuntimeControlActionResult, RuntimeControlEnvironment, RuntimeControlView, RuntimeEntryView, RuntimeLifecycleState, RuntimeServiceState, SearchConfigUpdate, SearchConfigView, SearchProviderConfigView, SearchProviderName, SearchProviderSpecView, SecretProviderEnvView, SecretProviderExecView, SecretProviderFileView, SecretProviderView, SecretRefView, SecretSourceView, SecretsConfigUpdate, SecretsView, ServerPathBreadcrumbView, ServerPathBrowseView, ServerPathEntryView, ServerPathReadView, SessionConfigView, SessionEntryView, SessionEventView, SessionHistoryView, SessionMessageView, SessionPatchUpdate, SessionPatchValidationError, SessionSkillEntryView, SessionTypeDescribeParams, SessionsListView, TavilySearchDepthValue, UiNcpAgent, UiNcpAssetPutView, UiNcpAssetView, UiNcpSessionListView, UiNcpSessionMessagesView, UiNcpSessionService, UiNcpStoredAssetRecord, type UiRemoteAccessHost, type UiRuntimeControlHost, UiServerEvent, UiServerHandle, UiServerOptions, buildConfigMeta, buildConfigSchemaView, buildConfigView, createCustomProvider, createUiRouter, deleteCustomProvider, deleteSession, ensureUiBridgeSecret, executeConfigAction, getSessionHistory, getUiBridgeSecretPath, listSessions, loadConfigOrDefault, patchSession, readUiBridgeSecret, startUiServer, testProviderConnection, updateChannel, updateModel, updateProvider, updateRuntime, updateSearch, updateSecrets };
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { compress } from "hono/compress";
3
3
  import { serve } from "@hono/node-server";
4
4
  import { WebSocket, WebSocketServer } from "ws";
5
5
  import fs, { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import { readFile, readdir, realpath, stat } from "node:fs/promises";
7
- import path, { dirname, isAbsolute, join, parse, resolve } from "node:path";
6
+ import { open, readFile, readdir, realpath, stat } from "node:fs/promises";
7
+ import path, { dirname, extname, isAbsolute, join, parse, resolve } from "node:path";
8
8
  import * as NextclawCore from "@nextclaw/core";
9
9
  import { ConfigSchema, DEFAULT_WORKSPACE_PATH, LiteLLMProvider, SessionManager, buildConfigSchema, createAgentProfile, expandHome, findEffectiveAgentProfile, getDataDir, getPackageVersion, getProviderName, getWorkspacePathFromConfig, hasSecretRef, isSensitiveConfigPath, loadConfig, normalizeThinkingLevels, parseThinkingLevel, probeFeishu, readAgentAvatarContent, removeAgentProfile, resolveEffectiveAgentProfiles, saveConfig, updateAgentProfile } from "@nextclaw/core";
10
10
  import { createHash, randomBytes, randomUUID, scryptSync, timingSafeEqual } from "node:crypto";
@@ -1331,7 +1331,7 @@ function buildConfigView(config, options) {
1331
1331
  const providers = {};
1332
1332
  for (const [name, provider] of Object.entries(config.providers)) providers[name] = toProviderView(config, provider, name, uiHints, findServerBuiltinProviderByName(name));
1333
1333
  return {
1334
- agents: config.agents,
1334
+ agents: sanitizePublicConfigValue(config.agents, "agents", uiHints),
1335
1335
  providers,
1336
1336
  search: buildSearchView(config),
1337
1337
  channels: sanitizePublicConfigValue(projectedChannels, "channels", uiHints),
@@ -1348,6 +1348,40 @@ function buildConfigView(config, options) {
1348
1348
  }
1349
1349
  };
1350
1350
  }
1351
+ function normalizeRuntimeEntries(entries) {
1352
+ if (!entries || typeof entries !== "object" || Array.isArray(entries)) return {};
1353
+ const normalized = {};
1354
+ for (const [rawId, rawEntry] of Object.entries(entries)) {
1355
+ const id = rawId.trim();
1356
+ if (!id || !rawEntry || typeof rawEntry !== "object" || Array.isArray(rawEntry)) continue;
1357
+ const entry = rawEntry;
1358
+ const type = normalizeOptionalString(entry.type);
1359
+ if (!type) continue;
1360
+ normalized[id] = {
1361
+ enabled: typeof entry.enabled === "boolean" ? entry.enabled : true,
1362
+ ...normalizeOptionalString(entry.label) ? { label: normalizeOptionalString(entry.label) ?? void 0 } : {},
1363
+ type,
1364
+ config: normalizeRuntimeEntryConfig(type, entry.config && typeof entry.config === "object" && !Array.isArray(entry.config) ? entry.config : {})
1365
+ };
1366
+ }
1367
+ return normalized;
1368
+ }
1369
+ function normalizeRuntimeEntryConfig(type, config) {
1370
+ if (type !== "narp-stdio") return { ...config };
1371
+ const command = normalizeOptionalString(config.command);
1372
+ const cwd = normalizeOptionalString(config.cwd);
1373
+ return {
1374
+ wireDialect: normalizeOptionalString(config.wireDialect) ?? "acp",
1375
+ processScope: normalizeOptionalString(config.processScope) ?? "per-session",
1376
+ ...command ? { command } : {},
1377
+ ...normalizeStringArray(config.args) ? { args: normalizeStringArray(config.args) } : {},
1378
+ env: normalizeUnknownStringRecord(config.env) ?? {},
1379
+ ...cwd ? { cwd } : {},
1380
+ startupTimeoutMs: normalizePositiveInteger(config.startupTimeoutMs) ?? 8e3,
1381
+ probeTimeoutMs: normalizePositiveInteger(config.probeTimeoutMs) ?? 3e3,
1382
+ requestTimeoutMs: normalizePositiveInteger(config.requestTimeoutMs) ?? 12e4
1383
+ };
1384
+ }
1351
1385
  function clearSecretRef(config, path) {
1352
1386
  if (config.secrets.refs[path]) delete config.secrets.refs[path];
1353
1387
  }
@@ -1545,6 +1579,22 @@ function normalizeOptionalString(value) {
1545
1579
  const trimmed = value.trim();
1546
1580
  return trimmed.length > 0 ? trimmed : null;
1547
1581
  }
1582
+ function normalizePositiveInteger(value) {
1583
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number.parseInt(value, 10) : NaN;
1584
+ if (!Number.isFinite(parsed)) return null;
1585
+ const normalized = Math.trunc(parsed);
1586
+ return normalized > 0 ? normalized : null;
1587
+ }
1588
+ function normalizeStringArray(value) {
1589
+ if (!Array.isArray(value)) return null;
1590
+ const entries = value.map((entry) => normalizeOptionalString(entry)).filter((entry) => Boolean(entry));
1591
+ return entries.length > 0 ? entries : null;
1592
+ }
1593
+ function normalizeUnknownStringRecord(value) {
1594
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1595
+ const entries = Object.entries(value).map(([key, entryValue]) => [key.trim(), normalizeOptionalString(entryValue)]).filter(([key, entryValue]) => key.length > 0 && Boolean(entryValue));
1596
+ return entries.length > 0 ? Object.fromEntries(entries) : null;
1597
+ }
1548
1598
  function normalizeHeaders(input) {
1549
1599
  if (!input) return null;
1550
1600
  const entries = Object.entries(input).map(([key, value]) => [key.trim(), String(value ?? "").trim()]).filter(([key, value]) => key.length > 0 && value.length > 0);
@@ -1858,6 +1908,7 @@ function updateRuntime(configPath, patch) {
1858
1908
  ...hasEngineConfig ? { engineConfig: { ...entry.engineConfig } } : {}
1859
1909
  };
1860
1910
  });
1911
+ if (patch.agents?.runtimes && Object.prototype.hasOwnProperty.call(patch.agents.runtimes, "entries")) config.agents.runtimes.entries = normalizeRuntimeEntries(patch.agents.runtimes.entries);
1861
1912
  if (Object.prototype.hasOwnProperty.call(patch, "bindings")) config.bindings = patch.bindings ?? [];
1862
1913
  if (patch.session) config.session = {
1863
1914
  ...config.session,
@@ -2591,6 +2642,7 @@ var ConfigRoutesController = class {
2591
2642
  if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "contextTokens")) changedPaths.push("agents.defaults.contextTokens");
2592
2643
  if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engine")) changedPaths.push("agents.defaults.engine");
2593
2644
  if (body.data.agents?.defaults && Object.prototype.hasOwnProperty.call(body.data.agents.defaults, "engineConfig")) changedPaths.push("agents.defaults.engineConfig");
2645
+ if (body.data.agents?.runtimes && Object.prototype.hasOwnProperty.call(body.data.agents.runtimes, "entries")) changedPaths.push("agents.runtimes.entries");
2594
2646
  changedPaths.push("agents.list", "bindings", "session");
2595
2647
  await this.publishConfigUpdates(changedPaths);
2596
2648
  return c.json(ok(result));
@@ -2988,6 +3040,16 @@ function applySessionTypePatch(metadata, patch) {
2988
3040
  delete metadata.session_type;
2989
3041
  delete metadata.sessionType;
2990
3042
  }
3043
+ function applyUiReadAtPatch(metadata, patch) {
3044
+ if (!Object.prototype.hasOwnProperty.call(patch, "uiReadAt")) return metadata;
3045
+ const uiReadAt = typeof patch.uiReadAt === "string" ? patch.uiReadAt.trim() : "";
3046
+ if (uiReadAt) return {
3047
+ ...metadata,
3048
+ ui_last_read_at: uiReadAt
3049
+ };
3050
+ const { ui_last_read_at: _removed, ...nextMetadata } = metadata;
3051
+ return nextMetadata;
3052
+ }
2991
3053
  async function applyProjectRootPatch(metadata, patch) {
2992
3054
  if (!Object.prototype.hasOwnProperty.call(patch, "projectRoot")) return;
2993
3055
  const projectRoot = await normalizeSessionProjectRoot(patch.projectRoot);
@@ -3000,14 +3062,16 @@ async function applyProjectRootPatch(metadata, patch) {
3000
3062
  delete metadata.projectRoot;
3001
3063
  }
3002
3064
  async function buildPatchedSessionMetadata(params) {
3065
+ const { metadata, patch } = params;
3003
3066
  const nextMetadata = applySessionPreferencePatch({
3004
- metadata: structuredClone(params.metadata),
3005
- patch: params.patch,
3067
+ metadata: structuredClone(metadata),
3068
+ patch,
3006
3069
  createInvalidThinkingError: () => /* @__PURE__ */ new Error("PREFERRED_THINKING_INVALID")
3007
3070
  });
3008
- applySessionTypePatch(nextMetadata, params.patch);
3009
- await applyProjectRootPatch(nextMetadata, params.patch);
3010
- return nextMetadata;
3071
+ applySessionTypePatch(nextMetadata, patch);
3072
+ const nextMetadataWithReadAt = applyUiReadAtPatch(nextMetadata, patch);
3073
+ await applyProjectRootPatch(nextMetadataWithReadAt, patch);
3074
+ return nextMetadataWithReadAt;
3011
3075
  }
3012
3076
  var NcpSessionRoutesController = class {
3013
3077
  sessionSkillsViewBuilder;
@@ -4343,6 +4407,41 @@ var RemoteRoutesController = class {
4343
4407
  };
4344
4408
  };
4345
4409
  //#endregion
4410
+ //#region src/ui/ui-routes/runtime-control.controller.ts
4411
+ var RuntimeControlRoutesController = class {
4412
+ constructor(host) {
4413
+ this.host = host;
4414
+ }
4415
+ getControl = async (c) => {
4416
+ try {
4417
+ return c.json(ok(await this.host.getControl()));
4418
+ } catch (error) {
4419
+ return c.json(err("RUNTIME_CONTROL_FAILED", formatUserFacingError(error)), 500);
4420
+ }
4421
+ };
4422
+ restartService = async (c) => {
4423
+ try {
4424
+ return c.json(ok(await this.host.restartService()));
4425
+ } catch (error) {
4426
+ return c.json(err("RUNTIME_RESTART_FAILED", formatUserFacingError(error)), 400);
4427
+ }
4428
+ };
4429
+ startService = async (c) => {
4430
+ try {
4431
+ return c.json(ok(await this.host.startService()));
4432
+ } catch (error) {
4433
+ return c.json(err("RUNTIME_START_FAILED", formatUserFacingError(error)), 400);
4434
+ }
4435
+ };
4436
+ stopService = async (c) => {
4437
+ try {
4438
+ return c.json(ok(await this.host.stopService()));
4439
+ } catch (error) {
4440
+ return c.json(err("RUNTIME_STOP_FAILED", formatUserFacingError(error)), 400);
4441
+ }
4442
+ };
4443
+ };
4444
+ //#endregion
4346
4445
  //#region src/ui/server-path/server-path-browse.utils.ts
4347
4446
  var ServerPathBrowseError = class extends Error {
4348
4447
  constructor(code, message) {
@@ -4429,6 +4528,139 @@ function isServerPathBrowseError(error) {
4429
4528
  return error instanceof ServerPathBrowseError;
4430
4529
  }
4431
4530
  //#endregion
4531
+ //#region src/ui/server-path/server-path-read.utils.ts
4532
+ const DEFAULT_PREVIEW_MAX_BYTES = 2e5;
4533
+ const MARKDOWN_EXTENSIONS = new Set([
4534
+ ".md",
4535
+ ".mdx",
4536
+ ".markdown"
4537
+ ]);
4538
+ const TEXT_EXTENSIONS = new Set([
4539
+ ".c",
4540
+ ".cc",
4541
+ ".conf",
4542
+ ".cpp",
4543
+ ".css",
4544
+ ".csv",
4545
+ ".env",
4546
+ ".go",
4547
+ ".graphql",
4548
+ ".h",
4549
+ ".hpp",
4550
+ ".html",
4551
+ ".ini",
4552
+ ".java",
4553
+ ".js",
4554
+ ".json",
4555
+ ".jsx",
4556
+ ".log",
4557
+ ".mjs",
4558
+ ".py",
4559
+ ".rb",
4560
+ ".rs",
4561
+ ".sh",
4562
+ ".sql",
4563
+ ".svg",
4564
+ ".toml",
4565
+ ".ts",
4566
+ ".tsx",
4567
+ ".txt",
4568
+ ".xml",
4569
+ ".yaml",
4570
+ ".yml"
4571
+ ]);
4572
+ var ServerPathReadError = class extends Error {
4573
+ constructor(code, message) {
4574
+ super(message);
4575
+ this.code = code;
4576
+ this.name = "ServerPathReadError";
4577
+ }
4578
+ };
4579
+ function normalizeReadPath(params) {
4580
+ const { basePath, path } = params;
4581
+ const rawPath = typeof path === "string" ? path.trim() : "";
4582
+ const normalizedBasePath = typeof basePath === "string" ? basePath.trim() : "";
4583
+ if (!rawPath) return resolve(expandHome(normalizedBasePath || homedir()));
4584
+ const expandedPath = expandHome(rawPath);
4585
+ if (expandedPath.startsWith("/")) return resolve(expandedPath);
4586
+ if (!normalizedBasePath) throw new ServerPathReadError("SERVER_PATH_BASE_REQUIRED", "relative server path requires a base path");
4587
+ return resolve(expandHome(normalizedBasePath), expandedPath);
4588
+ }
4589
+ function inferPreviewKind(path) {
4590
+ const extension = extname(path).toLowerCase();
4591
+ if (MARKDOWN_EXTENSIONS.has(extension)) return "markdown";
4592
+ if (TEXT_EXTENSIONS.has(extension)) return "text";
4593
+ return "binary";
4594
+ }
4595
+ function isLikelyTextBuffer(buffer) {
4596
+ for (const byte of buffer) if (byte === 0) return false;
4597
+ return true;
4598
+ }
4599
+ function readLanguageHint(path) {
4600
+ const extension = extname(path).toLowerCase();
4601
+ if (!extension) return null;
4602
+ if (MARKDOWN_EXTENSIONS.has(extension)) return "markdown";
4603
+ return extension.slice(1) || null;
4604
+ }
4605
+ async function readFilePreviewBytes(params) {
4606
+ const { maxBytes, resolvedPath, sizeBytes } = params;
4607
+ const bytesToRead = Math.min(sizeBytes, maxBytes);
4608
+ const fileHandle = await open(resolvedPath, "r");
4609
+ try {
4610
+ const buffer = Buffer.alloc(bytesToRead);
4611
+ const { bytesRead } = await fileHandle.read(buffer, 0, bytesToRead, 0);
4612
+ return {
4613
+ buffer: buffer.subarray(0, bytesRead),
4614
+ truncated: sizeBytes > maxBytes
4615
+ };
4616
+ } finally {
4617
+ await fileHandle.close();
4618
+ }
4619
+ }
4620
+ async function readServerPath(options = {}) {
4621
+ const requestedPath = typeof options.path === "string" ? options.path.trim() : "";
4622
+ const resolvedPath = normalizeReadPath(options);
4623
+ let resolvedStats;
4624
+ try {
4625
+ resolvedStats = await stat(resolvedPath);
4626
+ } catch {
4627
+ throw new ServerPathReadError("SERVER_PATH_NOT_FOUND", "server path does not exist");
4628
+ }
4629
+ if (!resolvedStats.isFile()) throw new ServerPathReadError("SERVER_PATH_NOT_FILE", "server path must point to a file");
4630
+ let previewBytes;
4631
+ try {
4632
+ previewBytes = await readFilePreviewBytes({
4633
+ resolvedPath,
4634
+ sizeBytes: resolvedStats.size,
4635
+ maxBytes: options.maxBytes ?? DEFAULT_PREVIEW_MAX_BYTES
4636
+ });
4637
+ } catch {
4638
+ throw new ServerPathReadError("SERVER_PATH_NOT_READABLE", "server path is not readable");
4639
+ }
4640
+ const inferredKind = inferPreviewKind(resolvedPath);
4641
+ const resolvedKind = inferredKind === "binary" && isLikelyTextBuffer(previewBytes.buffer) ? "text" : inferredKind;
4642
+ if (resolvedKind === "binary") return {
4643
+ requestedPath: requestedPath || resolvedPath,
4644
+ resolvedPath,
4645
+ kind: "binary",
4646
+ sizeBytes: resolvedStats.size,
4647
+ truncated: previewBytes.truncated,
4648
+ languageHint: readLanguageHint(resolvedPath)
4649
+ };
4650
+ return {
4651
+ requestedPath: requestedPath || resolvedPath,
4652
+ resolvedPath,
4653
+ kind: resolvedKind,
4654
+ sizeBytes: resolvedStats.size,
4655
+ truncated: previewBytes.truncated,
4656
+ text: previewBytes.buffer.toString("utf8"),
4657
+ languageHint: readLanguageHint(resolvedPath)
4658
+ };
4659
+ }
4660
+ function isServerPathReadError(error) {
4661
+ return error instanceof ServerPathReadError;
4662
+ }
4663
+ //#endregion
4432
4664
  //#region src/ui/ui-routes/server-path.controller.ts
4433
4665
  function readIncludeFilesFlag(value) {
4434
4666
  return value === "1" || value === "true";
@@ -4446,6 +4678,18 @@ var ServerPathRoutesController = class {
4446
4678
  throw error;
4447
4679
  }
4448
4680
  };
4681
+ read = async (c) => {
4682
+ try {
4683
+ const payload = await readServerPath({
4684
+ path: c.req.query("path"),
4685
+ basePath: c.req.query("basePath")
4686
+ });
4687
+ return c.json(ok(payload));
4688
+ } catch (error) {
4689
+ if (isServerPathReadError(error)) return c.json(err(error.code, error.message), 400);
4690
+ throw error;
4691
+ }
4692
+ };
4449
4693
  };
4450
4694
  //#endregion
4451
4695
  //#region src/ui/router.ts
@@ -4496,6 +4740,7 @@ function registerNcpSessionRoutes(app, ncpSessionController) {
4496
4740
  }
4497
4741
  function registerServerPathRoutes(app, serverPathController) {
4498
4742
  app.get("/api/server-paths/browse", serverPathController.browse);
4743
+ app.get("/api/server-paths/read", serverPathController.read);
4499
4744
  }
4500
4745
  function registerNcpRuntimeRoutes(app, options, ncpAssetController) {
4501
4746
  if (!options.ncpAgent) return;
@@ -4526,22 +4771,35 @@ function registerRemoteRoutes(app, remoteController) {
4526
4771
  app.put("/api/remote/settings", remoteController.updateSettings);
4527
4772
  app.post("/api/remote/service/:action", remoteController.controlService);
4528
4773
  }
4774
+ function registerRuntimeControlRoutes(app, runtimeControlController) {
4775
+ if (!runtimeControlController) return;
4776
+ app.get("/api/runtime/control", runtimeControlController.getControl);
4777
+ app.post("/api/runtime/control/start-service", runtimeControlController.startService);
4778
+ app.post("/api/runtime/control/restart-service", runtimeControlController.restartService);
4779
+ app.post("/api/runtime/control/stop-service", runtimeControlController.stopService);
4780
+ }
4781
+ function createUiRouteControllers(options, authService, marketplaceBaseUrl) {
4782
+ return {
4783
+ app: new AppRoutesController(options),
4784
+ agents: new AgentsRoutesController(options),
4785
+ auth: new AuthRoutesController(authService),
4786
+ config: new ConfigRoutesController(options),
4787
+ cron: new CronRoutesController(options),
4788
+ ncpSession: new NcpSessionRoutesController(options),
4789
+ ncpAsset: new NcpAssetRoutesController(options),
4790
+ serverPath: new ServerPathRoutesController(),
4791
+ remote: options.remoteAccess ? new RemoteRoutesController(options.remoteAccess) : null,
4792
+ runtimeControl: options.runtimeControl ? new RuntimeControlRoutesController(options.runtimeControl) : null,
4793
+ pluginMarketplace: new PluginMarketplaceController(options, marketplaceBaseUrl),
4794
+ skillMarketplace: new SkillMarketplaceController(options, marketplaceBaseUrl),
4795
+ mcpMarketplace: new McpMarketplaceController(options, marketplaceBaseUrl)
4796
+ };
4797
+ }
4529
4798
  function createUiRouter(options) {
4530
4799
  const app = new Hono();
4531
4800
  const marketplaceBaseUrl = normalizeMarketplaceBaseUrl(options);
4532
4801
  const authService = options.authService ?? new UiAuthService(options.configPath);
4533
- const appController = new AppRoutesController(options);
4534
- const agentsController = new AgentsRoutesController(options);
4535
- const authController = new AuthRoutesController(authService);
4536
- const configController = new ConfigRoutesController(options);
4537
- const cronController = new CronRoutesController(options);
4538
- const ncpSessionController = new NcpSessionRoutesController(options);
4539
- const ncpAssetController = new NcpAssetRoutesController(options);
4540
- const serverPathController = new ServerPathRoutesController();
4541
- const remoteController = options.remoteAccess ? new RemoteRoutesController(options.remoteAccess) : null;
4542
- const pluginMarketplaceController = new PluginMarketplaceController(options, marketplaceBaseUrl);
4543
- const skillMarketplaceController = new SkillMarketplaceController(options, marketplaceBaseUrl);
4544
- const mcpMarketplaceController = new McpMarketplaceController(options, marketplaceBaseUrl);
4802
+ const controllers = createUiRouteControllers(options, authService, marketplaceBaseUrl);
4545
4803
  app.notFound((c) => c.json(err("NOT_FOUND", "endpoint not found"), 404));
4546
4804
  app.use("/api/*", async (c, next) => {
4547
4805
  const path = c.req.path;
@@ -4556,21 +4814,22 @@ function createUiRouter(options) {
4556
4814
  c.status(401);
4557
4815
  return c.json(err("UNAUTHORIZED", "Authentication required."), 401);
4558
4816
  });
4559
- app.get("/api/health", appController.health);
4560
- app.get("/api/app/meta", appController.appMeta);
4561
- app.get("/api/runtime/bootstrap-status", appController.bootstrapStatus);
4562
- registerAuthRoutes(app, authController);
4563
- registerAgentRoutes(app, agentsController);
4564
- registerConfigRoutes(app, configController);
4565
- registerNcpSessionRoutes(app, ncpSessionController);
4566
- registerServerPathRoutes(app, serverPathController);
4567
- registerNcpRuntimeRoutes(app, options, ncpAssetController);
4568
- registerCronRoutes(app, cronController);
4569
- registerRemoteRoutes(app, remoteController);
4817
+ app.get("/api/health", controllers.app.health);
4818
+ app.get("/api/app/meta", controllers.app.appMeta);
4819
+ app.get("/api/runtime/bootstrap-status", controllers.app.bootstrapStatus);
4820
+ registerAuthRoutes(app, controllers.auth);
4821
+ registerAgentRoutes(app, controllers.agents);
4822
+ registerConfigRoutes(app, controllers.config);
4823
+ registerNcpSessionRoutes(app, controllers.ncpSession);
4824
+ registerServerPathRoutes(app, controllers.serverPath);
4825
+ registerNcpRuntimeRoutes(app, options, controllers.ncpAsset);
4826
+ registerCronRoutes(app, controllers.cron);
4827
+ registerRemoteRoutes(app, controllers.remote);
4828
+ registerRuntimeControlRoutes(app, controllers.runtimeControl);
4570
4829
  mountMarketplaceRoutes(app, {
4571
- plugin: pluginMarketplaceController,
4572
- skill: skillMarketplaceController,
4573
- mcp: mcpMarketplaceController
4830
+ plugin: controllers.pluginMarketplace,
4831
+ skill: controllers.skillMarketplace,
4832
+ mcp: controllers.mcpMarketplace
4574
4833
  });
4575
4834
  return app;
4576
4835
  }
@@ -4656,6 +4915,7 @@ function startUiServer(options) {
4656
4915
  ncpSessionService: options.ncpSessionService,
4657
4916
  authService,
4658
4917
  remoteAccess: options.remoteAccess,
4918
+ runtimeControl: options.runtimeControl,
4659
4919
  getBootstrapStatus: options.getBootstrapStatus,
4660
4920
  getPluginChannelBindings: options.getPluginChannelBindings,
4661
4921
  getPluginUiMetadata: options.getPluginUiMetadata
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextclaw/server",
3
- "version": "0.12.5",
3
+ "version": "0.12.7",
4
4
  "private": false,
5
5
  "description": "Nextclaw UI/API server.",
6
6
  "type": "module",
@@ -18,12 +18,12 @@
18
18
  "@hono/node-server": "^1.13.3",
19
19
  "hono": "^4.6.2",
20
20
  "ws": "^8.18.0",
21
- "@nextclaw/core": "0.12.5",
22
- "@nextclaw/mcp": "0.1.70",
23
- "@nextclaw/ncp-http-agent-server": "0.3.12",
24
- "@nextclaw/ncp": "0.5.0",
25
- "@nextclaw/openclaw-compat": "1.0.5",
26
- "@nextclaw/runtime": "0.2.37"
21
+ "@nextclaw/core": "0.12.7",
22
+ "@nextclaw/openclaw-compat": "1.0.7",
23
+ "@nextclaw/ncp": "0.5.2",
24
+ "@nextclaw/ncp-http-agent-server": "0.3.14",
25
+ "@nextclaw/mcp": "0.1.72",
26
+ "@nextclaw/runtime": "0.2.39"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^20.17.6",