nextclaw 0.13.2 → 0.13.4

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/cli/index.js CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  // src/cli/index.ts
4
4
  import { Command } from "commander";
5
- import { APP_NAME as APP_NAME5, APP_TAGLINE } from "@nextclaw/core";
5
+ import { APP_NAME as APP_NAME7, APP_TAGLINE } from "@nextclaw/core";
6
6
 
7
7
  // src/cli/runtime.ts
8
8
  import {
9
- loadConfig as loadConfig13,
10
- saveConfig as saveConfig9,
11
- getConfigPath as getConfigPath6,
9
+ loadConfig as loadConfig15,
10
+ saveConfig as saveConfig10,
11
+ getConfigPath as getConfigPath7,
12
12
  getDataDir as getDataDir9,
13
13
  ConfigSchema as ConfigSchema2,
14
14
  getWorkspacePath as getWorkspacePath10,
@@ -17,7 +17,7 @@ import {
17
17
  AgentLoop,
18
18
  ProviderManager as ProviderManager2,
19
19
  resolveConfigSecrets as resolveConfigSecrets3,
20
- APP_NAME as APP_NAME4,
20
+ APP_NAME as APP_NAME6,
21
21
  DEFAULT_WORKSPACE_DIR,
22
22
  DEFAULT_WORKSPACE_PATH
23
23
  } from "@nextclaw/core";
@@ -29,7 +29,7 @@ import {
29
29
  import { existsSync as existsSync13, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7 } from "fs";
30
30
  import { join as join9, resolve as resolve12 } from "path";
31
31
  import { createInterface as createInterface3 } from "readline";
32
- import { fileURLToPath as fileURLToPath4 } from "url";
32
+ import { fileURLToPath as fileURLToPath5 } from "url";
33
33
  import { spawn as spawn3 } from "child_process";
34
34
 
35
35
  // src/cli/restart-coordinator.ts
@@ -822,6 +822,15 @@ function writeServiceState(state) {
822
822
  mkdirSync3(resolve4(path2, ".."), { recursive: true });
823
823
  writeFileSync3(path2, JSON.stringify(state, null, 2));
824
824
  }
825
+ function updateServiceState(updater) {
826
+ const current = readServiceState();
827
+ if (!current) {
828
+ return null;
829
+ }
830
+ const next = updater(current);
831
+ writeServiceState(next);
832
+ return next;
833
+ }
825
834
  function clearServiceState() {
826
835
  const path2 = resolveServiceStatePath();
827
836
  if (existsSync4(path2)) {
@@ -1101,6 +1110,7 @@ function appendPluginCapabilityLines(lines, plugin) {
1101
1110
  // src/cli/commands/dev-first-party-plugin-load-paths.ts
1102
1111
  import fs from "fs";
1103
1112
  import path from "path";
1113
+ import { fileURLToPath as fileURLToPath2 } from "url";
1104
1114
  var readJsonFile = (filePath) => {
1105
1115
  try {
1106
1116
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -1117,6 +1127,14 @@ var readString = (value) => {
1117
1127
  const trimmed = value.trim();
1118
1128
  return trimmed || void 0;
1119
1129
  };
1130
+ var resolveDevFirstPartyPluginDir = (explicitDir, moduleDir = path.dirname(fileURLToPath2(import.meta.url))) => {
1131
+ const configured = explicitDir?.trim();
1132
+ if (configured) {
1133
+ return configured;
1134
+ }
1135
+ const inferred = path.resolve(moduleDir, "../../../../extensions");
1136
+ return fs.existsSync(inferred) ? inferred : void 0;
1137
+ };
1120
1138
  var hasOpenClawExtensions = (pkg) => {
1121
1139
  const openclaw = pkg.openclaw;
1122
1140
  if (!openclaw || typeof openclaw !== "object" || Array.isArray(openclaw)) {
@@ -1168,7 +1186,7 @@ var readWorkspacePluginPackages = (workspaceExtensionsDir) => {
1168
1186
  return packages;
1169
1187
  };
1170
1188
  var resolveDevFirstPartyPluginLoadPaths = (config2, workspaceExtensionsDir) => {
1171
- const rootDir = workspaceExtensionsDir?.trim();
1189
+ const rootDir = resolveDevFirstPartyPluginDir(workspaceExtensionsDir);
1172
1190
  if (!rootDir) {
1173
1191
  return [];
1174
1192
  }
@@ -1193,7 +1211,7 @@ var resolveDevFirstPartyPluginLoadPaths = (config2, workspaceExtensionsDir) => {
1193
1211
  return loadPaths;
1194
1212
  };
1195
1213
  var resolveDevFirstPartyPluginInstallRoots = (config2, workspaceExtensionsDir) => {
1196
- const rootDir = workspaceExtensionsDir?.trim();
1214
+ const rootDir = resolveDevFirstPartyPluginDir(workspaceExtensionsDir);
1197
1215
  if (!rootDir) {
1198
1216
  return [];
1199
1217
  }
@@ -1480,7 +1498,7 @@ function toExtensionRegistry(pluginRegistry) {
1480
1498
 
1481
1499
  // src/cli/commands/plugins.ts
1482
1500
  function loadPluginRegistry(config2, workspaceDir) {
1483
- const workspaceExtensionsDir = process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR;
1501
+ const workspaceExtensionsDir = resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR);
1484
1502
  const configWithDevPluginPaths = applyDevFirstPartyPluginLoadPaths(
1485
1503
  config2,
1486
1504
  workspaceExtensionsDir
@@ -3016,11 +3034,11 @@ var PlatformAuthCommands = class {
3016
3034
  };
3017
3035
 
3018
3036
  // src/cli/commands/remote.ts
3019
- import { getConfigPath as getConfigPath3, getDataDir as getDataDir4, loadConfig as loadConfig8 } from "@nextclaw/core";
3037
+ import { getConfigPath as getConfigPath4, loadConfig as loadConfig10, saveConfig as saveConfig7 } from "@nextclaw/core";
3038
+ import { hostname as hostname3 } from "os";
3039
+
3040
+ // src/cli/remote/remote-relay-bridge.ts
3020
3041
  import { ensureUiBridgeSecret } from "@nextclaw/server";
3021
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
3022
- import { dirname as dirname2, join as join4 } from "path";
3023
- import { hostname, platform as readPlatform } from "os";
3024
3042
  function encodeBase64(bytes) {
3025
3043
  return Buffer.from(bytes).toString("base64");
3026
3044
  }
@@ -3030,9 +3048,122 @@ function decodeBase64(base64) {
3030
3048
  }
3031
3049
  return new Uint8Array(Buffer.from(base64, "base64"));
3032
3050
  }
3033
- function delay(ms) {
3034
- return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
3035
- }
3051
+ var RemoteRelayBridge = class {
3052
+ constructor(localOrigin) {
3053
+ this.localOrigin = localOrigin;
3054
+ }
3055
+ async ensureLocalUiHealthy() {
3056
+ const response = await fetch(`${this.localOrigin}/api/health`);
3057
+ if (!response.ok) {
3058
+ throw new Error(`Local UI is not healthy at ${this.localOrigin}. Start NextClaw first.`);
3059
+ }
3060
+ }
3061
+ async forward(frame, socket) {
3062
+ const bridgeCookie = await this.requestBridgeCookie();
3063
+ const url = new URL(frame.path, this.localOrigin);
3064
+ const headers = this.createForwardHeaders(frame.headers, bridgeCookie);
3065
+ const response = await fetch(url, {
3066
+ method: frame.method,
3067
+ headers,
3068
+ body: frame.method === "GET" || frame.method === "HEAD" ? void 0 : decodeBase64(frame.bodyBase64)
3069
+ });
3070
+ const responseHeaders = Array.from(response.headers.entries()).filter(([key]) => {
3071
+ const lower = key.toLowerCase();
3072
+ return !["content-length", "connection", "transfer-encoding", "set-cookie"].includes(lower);
3073
+ });
3074
+ const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
3075
+ if (response.body && contentType.startsWith("text/event-stream")) {
3076
+ await this.sendStreamingResponse({ frame, response, responseHeaders, socket });
3077
+ return;
3078
+ }
3079
+ const responseBody = response.body ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array();
3080
+ socket.send(JSON.stringify({
3081
+ type: "response",
3082
+ requestId: frame.requestId,
3083
+ status: response.status,
3084
+ headers: responseHeaders,
3085
+ bodyBase64: encodeBase64(responseBody)
3086
+ }));
3087
+ }
3088
+ createForwardHeaders(headersList, bridgeCookie) {
3089
+ const headers = new Headers();
3090
+ for (const [key, value] of headersList) {
3091
+ const lower = key.toLowerCase();
3092
+ if ([
3093
+ "host",
3094
+ "connection",
3095
+ "content-length",
3096
+ "cookie",
3097
+ "x-forwarded-for",
3098
+ "x-forwarded-proto",
3099
+ "cf-connecting-ip"
3100
+ ].includes(lower)) {
3101
+ continue;
3102
+ }
3103
+ headers.set(key, value);
3104
+ }
3105
+ if (bridgeCookie) {
3106
+ headers.set("cookie", bridgeCookie);
3107
+ }
3108
+ return headers;
3109
+ }
3110
+ async requestBridgeCookie() {
3111
+ const response = await fetch(`${this.localOrigin}/api/auth/bridge`, {
3112
+ method: "POST",
3113
+ headers: {
3114
+ "x-nextclaw-ui-bridge-secret": ensureUiBridgeSecret()
3115
+ }
3116
+ });
3117
+ const payload = await response.json();
3118
+ if (!response.ok || !payload.ok) {
3119
+ throw new Error(payload.error?.message ?? `Failed to request local auth bridge (${response.status}).`);
3120
+ }
3121
+ return typeof payload.data?.cookie === "string" && payload.data.cookie.trim().length > 0 ? payload.data.cookie.trim() : null;
3122
+ }
3123
+ async sendStreamingResponse(params) {
3124
+ params.socket.send(JSON.stringify({
3125
+ type: "response.start",
3126
+ requestId: params.frame.requestId,
3127
+ status: params.response.status,
3128
+ headers: params.responseHeaders
3129
+ }));
3130
+ const reader = params.response.body?.getReader();
3131
+ if (!reader) {
3132
+ params.socket.send(JSON.stringify({
3133
+ type: "response.end",
3134
+ requestId: params.frame.requestId
3135
+ }));
3136
+ return;
3137
+ }
3138
+ try {
3139
+ while (true) {
3140
+ const { value, done } = await reader.read();
3141
+ if (done) {
3142
+ break;
3143
+ }
3144
+ if (value && value.length > 0) {
3145
+ params.socket.send(JSON.stringify({
3146
+ type: "response.chunk",
3147
+ requestId: params.frame.requestId,
3148
+ bodyBase64: encodeBase64(value)
3149
+ }));
3150
+ }
3151
+ }
3152
+ } finally {
3153
+ reader.releaseLock();
3154
+ }
3155
+ params.socket.send(JSON.stringify({
3156
+ type: "response.end",
3157
+ requestId: params.frame.requestId
3158
+ }));
3159
+ }
3160
+ };
3161
+
3162
+ // src/cli/remote/remote-platform-client.ts
3163
+ import { getConfigPath as getConfigPath3, getDataDir as getDataDir4, loadConfig as loadConfig8 } from "@nextclaw/core";
3164
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
3165
+ import { dirname as dirname2, join as join4 } from "path";
3166
+ import { hostname, platform as readPlatform } from "os";
3036
3167
  function ensureDir(path2) {
3037
3168
  mkdirSync4(path2, { recursive: true });
3038
3169
  }
@@ -3051,9 +3182,82 @@ function writeJsonFile(path2, value) {
3051
3182
  writeFileSync4(path2, `${JSON.stringify(value, null, 2)}
3052
3183
  `, "utf-8");
3053
3184
  }
3054
- var RemoteCommands = class {
3185
+ function maskToken(value) {
3186
+ if (value.length <= 12) {
3187
+ return "<redacted>";
3188
+ }
3189
+ return `${value.slice(0, 6)}...${value.slice(-4)}`;
3190
+ }
3191
+ function normalizeOptionalString3(value) {
3192
+ if (typeof value !== "string") {
3193
+ return void 0;
3194
+ }
3195
+ const trimmed = value.trim();
3196
+ return trimmed.length > 0 ? trimmed : void 0;
3197
+ }
3198
+ function delay(ms, signal) {
3199
+ return new Promise((resolveDelay, rejectDelay) => {
3200
+ const timer = setTimeout(() => {
3201
+ signal?.removeEventListener("abort", onAbort);
3202
+ resolveDelay();
3203
+ }, ms);
3204
+ const onAbort = () => {
3205
+ clearTimeout(timer);
3206
+ rejectDelay(new Error("Remote connector aborted."));
3207
+ };
3208
+ if (signal) {
3209
+ signal.addEventListener("abort", onAbort, { once: true });
3210
+ }
3211
+ });
3212
+ }
3213
+ function redactWsUrl(url) {
3214
+ try {
3215
+ const parsed = new URL(url);
3216
+ const token = parsed.searchParams.get("token");
3217
+ if (token) {
3218
+ parsed.searchParams.set("token", maskToken(token));
3219
+ }
3220
+ return parsed.toString();
3221
+ } catch {
3222
+ return url;
3223
+ }
3224
+ }
3225
+ var RemotePlatformClient = class {
3055
3226
  remoteDir = join4(getDataDir4(), "remote");
3056
3227
  devicePath = join4(this.remoteDir, "device.json");
3228
+ resolveRunContext(opts) {
3229
+ const { platformBase, token, config: config2 } = this.resolvePlatformAccess(opts);
3230
+ return {
3231
+ config: config2,
3232
+ platformBase,
3233
+ token,
3234
+ localOrigin: this.resolveLocalOrigin(config2, opts),
3235
+ displayName: this.resolveDisplayName(config2, opts),
3236
+ deviceInstallId: this.ensureDeviceInstallId(),
3237
+ autoReconnect: opts.once ? false : opts.autoReconnect ?? config2.remote.autoReconnect
3238
+ };
3239
+ }
3240
+ async registerDevice(params) {
3241
+ const response = await fetch(`${params.platformBase}/platform/remote/devices/register`, {
3242
+ method: "POST",
3243
+ headers: {
3244
+ "content-type": "application/json",
3245
+ authorization: `Bearer ${params.token}`
3246
+ },
3247
+ body: JSON.stringify({
3248
+ deviceInstallId: params.deviceInstallId,
3249
+ displayName: params.displayName,
3250
+ platform: readPlatform(),
3251
+ appVersion: getPackageVersion(),
3252
+ localOrigin: params.localOrigin
3253
+ })
3254
+ });
3255
+ const payload = await response.json();
3256
+ if (!response.ok || !payload.ok || !payload.data?.device) {
3257
+ throw new Error(payload.error?.message ?? `Failed to register remote device (${response.status}).`);
3258
+ }
3259
+ return payload.data.device;
3260
+ }
3057
3261
  ensureDeviceInstallId() {
3058
3262
  const existing = readJsonFile2(this.devicePath);
3059
3263
  if (existing?.deviceInstallId?.trim()) {
@@ -3072,10 +3276,10 @@ var RemoteCommands = class {
3072
3276
  if (!token) {
3073
3277
  throw new Error('NextClaw platform token is missing. Run "nextclaw login" first.');
3074
3278
  }
3075
- const configuredApiBase = typeof nextclawProvider?.apiBase === "string" ? nextclawProvider.apiBase.trim() : "";
3076
- const rawApiBase = typeof opts.apiBase === "string" && opts.apiBase.trim().length > 0 ? opts.apiBase.trim() : configuredApiBase;
3279
+ const configuredApiBase = normalizeOptionalString3(config2.remote.platformApiBase) ?? (typeof nextclawProvider?.apiBase === "string" ? nextclawProvider.apiBase.trim() : "");
3280
+ const rawApiBase = normalizeOptionalString3(opts.apiBase) ?? configuredApiBase;
3077
3281
  if (!rawApiBase) {
3078
- throw new Error("Platform API base is missing. Pass --api-base or run nextclaw login.");
3282
+ throw new Error("Platform API base is missing. Pass --api-base, run nextclaw login, or set remote.platformApiBase.");
3079
3283
  }
3080
3284
  const { platformBase } = resolvePlatformApiBase({
3081
3285
  explicitApiBase: rawApiBase,
@@ -3084,8 +3288,9 @@ var RemoteCommands = class {
3084
3288
  return { platformBase, token, config: config2 };
3085
3289
  }
3086
3290
  resolveLocalOrigin(config2, opts) {
3087
- if (typeof opts.localOrigin === "string" && opts.localOrigin.trim().length > 0) {
3088
- return opts.localOrigin.trim().replace(/\/$/, "");
3291
+ const explicitOrigin = normalizeOptionalString3(opts.localOrigin);
3292
+ if (explicitOrigin) {
3293
+ return explicitOrigin.replace(/\/$/, "");
3089
3294
  }
3090
3295
  const state = readServiceState();
3091
3296
  if (state && isProcessRunning(state.pid) && Number.isFinite(state.uiPort)) {
@@ -3094,191 +3299,413 @@ var RemoteCommands = class {
3094
3299
  const configuredPort = typeof config2.ui?.port === "number" && Number.isFinite(config2.ui.port) ? config2.ui.port : 18791;
3095
3300
  return `http://127.0.0.1:${configuredPort}`;
3096
3301
  }
3097
- async ensureLocalUiHealthy(localOrigin) {
3098
- const response = await fetch(`${localOrigin}/api/health`);
3099
- if (!response.ok) {
3100
- throw new Error(`Local UI is not healthy at ${localOrigin}. Start NextClaw first.`);
3101
- }
3302
+ resolveDisplayName(config2, opts) {
3303
+ return normalizeOptionalString3(opts.name) ?? normalizeOptionalString3(config2.remote.deviceName) ?? hostname();
3102
3304
  }
3103
- async registerDevice(params) {
3104
- const response = await fetch(`${params.platformBase}/platform/remote/devices/register`, {
3105
- method: "POST",
3106
- headers: {
3107
- "content-type": "application/json",
3108
- authorization: `Bearer ${params.token}`
3109
- },
3110
- body: JSON.stringify({
3111
- deviceInstallId: params.deviceInstallId,
3112
- displayName: params.displayName,
3113
- platform: readPlatform(),
3114
- appVersion: getPackageVersion(),
3115
- localOrigin: params.localOrigin
3116
- })
3117
- });
3118
- const payload = await response.json();
3119
- if (!response.ok || !payload.ok || !payload.data?.device) {
3120
- throw new Error(payload.error?.message ?? `Failed to register remote device (${response.status}).`);
3121
- }
3122
- return payload.data.device;
3305
+ };
3306
+
3307
+ // src/cli/remote/remote-connector.ts
3308
+ var RemoteConnector = class {
3309
+ constructor(logger = console) {
3310
+ this.logger = logger;
3123
3311
  }
3124
- async requestBridgeCookie(localOrigin) {
3125
- const response = await fetch(`${localOrigin}/api/auth/bridge`, {
3126
- method: "POST",
3127
- headers: {
3128
- "x-nextclaw-ui-bridge-secret": ensureUiBridgeSecret()
3312
+ platformClient = new RemotePlatformClient();
3313
+ async connectOnce(params) {
3314
+ return await new Promise((resolve13, reject) => {
3315
+ const socket = new WebSocket(params.wsUrl);
3316
+ let settled = false;
3317
+ let aborted = false;
3318
+ const pingTimer = setInterval(() => {
3319
+ if (socket.readyState === WebSocket.OPEN) {
3320
+ socket.send(JSON.stringify({ type: "ping", at: (/* @__PURE__ */ new Date()).toISOString() }));
3321
+ }
3322
+ }, 15e3);
3323
+ const cleanup = () => {
3324
+ clearInterval(pingTimer);
3325
+ params.signal?.removeEventListener("abort", onAbort);
3326
+ };
3327
+ const finishResolve = (value) => {
3328
+ if (settled) {
3329
+ return;
3330
+ }
3331
+ settled = true;
3332
+ cleanup();
3333
+ resolve13(value);
3334
+ };
3335
+ const finishReject = (error) => {
3336
+ if (settled) {
3337
+ return;
3338
+ }
3339
+ settled = true;
3340
+ cleanup();
3341
+ reject(error);
3342
+ };
3343
+ const onAbort = () => {
3344
+ aborted = true;
3345
+ try {
3346
+ socket.close(1e3, "Remote connector aborted");
3347
+ } catch {
3348
+ finishResolve("aborted");
3349
+ }
3350
+ };
3351
+ if (params.signal) {
3352
+ if (params.signal.aborted) {
3353
+ onAbort();
3354
+ } else {
3355
+ params.signal.addEventListener("abort", onAbort, { once: true });
3356
+ }
3129
3357
  }
3358
+ socket.addEventListener("open", () => {
3359
+ params.statusStore?.write({
3360
+ enabled: true,
3361
+ state: "connected",
3362
+ deviceId: params.deviceId,
3363
+ deviceName: params.displayName,
3364
+ platformBase: params.platformBase,
3365
+ localOrigin: params.localOrigin,
3366
+ lastConnectedAt: (/* @__PURE__ */ new Date()).toISOString(),
3367
+ lastError: null
3368
+ });
3369
+ this.logger.info(`\u2713 Remote connector connected: ${redactWsUrl(params.wsUrl)}`);
3370
+ });
3371
+ socket.addEventListener("message", (event) => {
3372
+ this.handleSocketMessage({ data: event.data, relayBridge: params.relayBridge, socket });
3373
+ });
3374
+ socket.addEventListener("close", () => {
3375
+ finishResolve(aborted ? "aborted" : "closed");
3376
+ });
3377
+ socket.addEventListener("error", () => {
3378
+ if (aborted) {
3379
+ finishResolve("aborted");
3380
+ return;
3381
+ }
3382
+ finishReject(new Error("Remote connector websocket failed."));
3383
+ });
3130
3384
  });
3131
- const payload = await response.json();
3132
- if (!response.ok || !payload.ok) {
3133
- throw new Error(payload.error?.message ?? `Failed to request local auth bridge (${response.status}).`);
3134
- }
3135
- return typeof payload.data?.cookie === "string" && payload.data.cookie.trim().length > 0 ? payload.data.cookie.trim() : null;
3136
3385
  }
3137
- async handleRelayRequest(params) {
3138
- const bridgeCookie = await this.requestBridgeCookie(params.localOrigin);
3139
- const url = new URL(params.frame.path, params.localOrigin);
3140
- const headers = new Headers();
3141
- for (const [key, value] of params.frame.headers) {
3142
- const lower = key.toLowerCase();
3143
- if ([
3144
- "host",
3145
- "connection",
3146
- "content-length",
3147
- "cookie",
3148
- "x-forwarded-for",
3149
- "x-forwarded-proto",
3150
- "cf-connecting-ip"
3151
- ].includes(lower)) {
3152
- continue;
3386
+ handleSocketMessage(params) {
3387
+ void (async () => {
3388
+ const frame = this.parseRelayFrame(params.data);
3389
+ if (!frame) {
3390
+ return;
3153
3391
  }
3154
- headers.set(key, value);
3392
+ try {
3393
+ await params.relayBridge.forward(frame, params.socket);
3394
+ } catch (error) {
3395
+ params.socket.send(JSON.stringify({
3396
+ type: "response.error",
3397
+ requestId: frame.requestId,
3398
+ message: error instanceof Error ? error.message : String(error)
3399
+ }));
3400
+ }
3401
+ })();
3402
+ }
3403
+ parseRelayFrame(data) {
3404
+ try {
3405
+ const frame = JSON.parse(String(data ?? ""));
3406
+ return frame.type === "request" ? frame : null;
3407
+ } catch {
3408
+ return null;
3155
3409
  }
3156
- if (bridgeCookie) {
3157
- headers.set("cookie", bridgeCookie);
3410
+ }
3411
+ async ensureDevice(params) {
3412
+ if (params.device) {
3413
+ return params.device;
3158
3414
  }
3159
- const bodyBytes = decodeBase64(params.frame.bodyBase64);
3160
- const response = await fetch(url, {
3161
- method: params.frame.method,
3162
- headers,
3163
- body: params.frame.method === "GET" || params.frame.method === "HEAD" ? void 0 : bodyBytes
3415
+ const device = await this.platformClient.registerDevice({
3416
+ platformBase: params.context.platformBase,
3417
+ token: params.context.token,
3418
+ deviceInstallId: params.context.deviceInstallId,
3419
+ displayName: params.context.displayName,
3420
+ localOrigin: params.context.localOrigin
3164
3421
  });
3165
- const responseHeaders = Array.from(response.headers.entries()).filter(([key]) => {
3166
- const lower = key.toLowerCase();
3167
- return !["content-length", "connection", "transfer-encoding", "set-cookie"].includes(lower);
3168
- });
3169
- const contentType = response.headers.get("content-type")?.toLowerCase() ?? "";
3170
- if (response.body && contentType.startsWith("text/event-stream")) {
3171
- params.socket.send(JSON.stringify({
3172
- type: "response.start",
3173
- requestId: params.frame.requestId,
3174
- status: response.status,
3175
- headers: responseHeaders
3176
- }));
3177
- const reader = response.body.getReader();
3422
+ this.logger.info(`\u2713 Remote device registered: ${device.displayName} (${device.id})`);
3423
+ this.logger.info(`\u2713 Local origin: ${params.context.localOrigin}`);
3424
+ this.logger.info(`\u2713 Platform: ${params.context.platformBase}`);
3425
+ return device;
3426
+ }
3427
+ writeRemoteState(statusStore, next) {
3428
+ statusStore?.write(next);
3429
+ }
3430
+ async runCycle(params) {
3431
+ try {
3432
+ this.writeRemoteState(params.opts.statusStore, {
3433
+ enabled: true,
3434
+ state: "connecting",
3435
+ deviceId: params.device?.id,
3436
+ deviceName: params.context.displayName,
3437
+ platformBase: params.context.platformBase,
3438
+ localOrigin: params.context.localOrigin,
3439
+ lastError: null
3440
+ });
3441
+ const device = await this.ensureDevice({ device: params.device, context: params.context });
3442
+ const wsUrl = `${params.context.platformBase.replace(/^http/i, "ws")}/platform/remote/connect?deviceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(params.context.token)}`;
3443
+ const outcome = await this.connectOnce({
3444
+ wsUrl,
3445
+ relayBridge: params.relayBridge,
3446
+ signal: params.opts.signal,
3447
+ statusStore: params.opts.statusStore,
3448
+ displayName: params.context.displayName,
3449
+ deviceId: device.id,
3450
+ platformBase: params.context.platformBase,
3451
+ localOrigin: params.context.localOrigin
3452
+ });
3453
+ if (outcome !== "aborted") {
3454
+ this.writeRemoteState(params.opts.statusStore, {
3455
+ enabled: true,
3456
+ state: "disconnected",
3457
+ deviceId: device.id,
3458
+ deviceName: params.context.displayName,
3459
+ platformBase: params.context.platformBase,
3460
+ localOrigin: params.context.localOrigin,
3461
+ lastError: null
3462
+ });
3463
+ }
3464
+ return { device, aborted: outcome === "aborted" };
3465
+ } catch (error) {
3466
+ const message = error instanceof Error ? error.message : String(error);
3467
+ this.writeRemoteState(params.opts.statusStore, {
3468
+ enabled: true,
3469
+ state: "error",
3470
+ deviceId: params.device?.id,
3471
+ deviceName: params.context.displayName,
3472
+ platformBase: params.context.platformBase,
3473
+ localOrigin: params.context.localOrigin,
3474
+ lastError: message
3475
+ });
3476
+ this.logger.error(`Remote connector error: ${message}`);
3477
+ return { device: params.device, aborted: false };
3478
+ }
3479
+ }
3480
+ async run(opts = {}) {
3481
+ const context = this.platformClient.resolveRunContext(opts);
3482
+ const relayBridge = new RemoteRelayBridge(context.localOrigin);
3483
+ await relayBridge.ensureLocalUiHealthy();
3484
+ let device = null;
3485
+ while (!opts.signal?.aborted) {
3486
+ const cycle = await this.runCycle({ device, context, relayBridge, opts });
3487
+ device = cycle.device;
3488
+ if (cycle.aborted || !context.autoReconnect || opts.signal?.aborted) {
3489
+ break;
3490
+ }
3491
+ this.logger.warn("Remote connector disconnected. Reconnecting in 3s...");
3178
3492
  try {
3179
- while (true) {
3180
- const { value, done } = await reader.read();
3181
- if (done) {
3182
- break;
3183
- }
3184
- if (value && value.length > 0) {
3185
- params.socket.send(JSON.stringify({
3186
- type: "response.chunk",
3187
- requestId: params.frame.requestId,
3188
- bodyBase64: encodeBase64(value)
3189
- }));
3190
- }
3191
- }
3192
- } finally {
3193
- reader.releaseLock();
3493
+ await delay(3e3, opts.signal);
3494
+ } catch {
3495
+ break;
3194
3496
  }
3195
- params.socket.send(JSON.stringify({
3196
- type: "response.end",
3197
- requestId: params.frame.requestId
3198
- }));
3199
- return;
3200
3497
  }
3201
- const responseBody = response.body ? new Uint8Array(await response.arrayBuffer()) : new Uint8Array();
3202
- params.socket.send(JSON.stringify({
3203
- type: "response",
3204
- requestId: params.frame.requestId,
3205
- status: response.status,
3206
- headers: responseHeaders,
3207
- bodyBase64: encodeBase64(responseBody)
3498
+ this.writeRemoteState(opts.statusStore, {
3499
+ enabled: opts.mode === "service" ? true : Boolean(context.config.remote.enabled),
3500
+ state: opts.signal?.aborted ? "disconnected" : "disabled",
3501
+ deviceId: device?.id,
3502
+ deviceName: context.displayName,
3503
+ platformBase: context.platformBase,
3504
+ localOrigin: context.localOrigin,
3505
+ lastError: null
3506
+ });
3507
+ }
3508
+ };
3509
+
3510
+ // src/cli/remote/remote-status-store.ts
3511
+ import { hostname as hostname2 } from "os";
3512
+ import { loadConfig as loadConfig9 } from "@nextclaw/core";
3513
+ function normalizeOptionalString4(value) {
3514
+ if (typeof value !== "string") {
3515
+ return void 0;
3516
+ }
3517
+ const trimmed = value.trim();
3518
+ return trimmed.length > 0 ? trimmed : void 0;
3519
+ }
3520
+ function buildConfiguredRemoteState(config2 = loadConfig9()) {
3521
+ const remote = config2.remote;
3522
+ return {
3523
+ enabled: Boolean(remote.enabled),
3524
+ mode: "service",
3525
+ state: remote.enabled ? "disconnected" : "disabled",
3526
+ ...normalizeOptionalString4(remote.deviceName) ? { deviceName: normalizeOptionalString4(remote.deviceName) } : {},
3527
+ ...normalizeOptionalString4(remote.platformApiBase) ? { platformBase: normalizeOptionalString4(remote.platformApiBase) } : {},
3528
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3529
+ };
3530
+ }
3531
+ function resolveRemoteStatusSnapshot(config2 = loadConfig9()) {
3532
+ const serviceState = readServiceState();
3533
+ if (serviceState?.remote) {
3534
+ return {
3535
+ configuredEnabled: Boolean(config2.remote.enabled),
3536
+ runtime: serviceState.remote
3537
+ };
3538
+ }
3539
+ if (config2.remote.enabled) {
3540
+ return {
3541
+ configuredEnabled: true,
3542
+ runtime: {
3543
+ ...buildConfiguredRemoteState(config2),
3544
+ deviceName: normalizeOptionalString4(config2.remote.deviceName) ?? hostname2()
3545
+ }
3546
+ };
3547
+ }
3548
+ return {
3549
+ configuredEnabled: false,
3550
+ runtime: null
3551
+ };
3552
+ }
3553
+ var RemoteStatusStore = class {
3554
+ constructor(mode) {
3555
+ this.mode = mode;
3556
+ }
3557
+ write(next) {
3558
+ updateServiceState((state) => ({
3559
+ ...state,
3560
+ remote: {
3561
+ ...state.remote,
3562
+ ...next,
3563
+ mode: this.mode,
3564
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
3565
+ }
3208
3566
  }));
3209
3567
  }
3210
- async connectOnce(params) {
3211
- await new Promise((resolve13, reject) => {
3212
- const socket = new WebSocket(params.wsUrl);
3213
- const pingTimer = setInterval(() => {
3214
- if (socket.readyState === WebSocket.OPEN) {
3215
- socket.send(JSON.stringify({ type: "ping", at: (/* @__PURE__ */ new Date()).toISOString() }));
3216
- }
3217
- }, 15e3);
3218
- socket.addEventListener("open", () => {
3219
- console.log(`\u2713 Remote connector connected: ${params.wsUrl}`);
3220
- });
3221
- socket.addEventListener("message", (event) => {
3222
- void (async () => {
3223
- let frame = null;
3224
- try {
3225
- frame = JSON.parse(String(event.data ?? ""));
3226
- } catch {
3227
- return;
3228
- }
3229
- if (!frame || frame.type !== "request") {
3230
- return;
3231
- }
3232
- try {
3233
- await this.handleRelayRequest({ frame, localOrigin: params.localOrigin, socket });
3234
- } catch (error) {
3235
- socket.send(JSON.stringify({
3236
- type: "response.error",
3237
- requestId: frame.requestId,
3238
- message: error instanceof Error ? error.message : String(error)
3239
- }));
3240
- }
3241
- })();
3242
- });
3243
- socket.addEventListener("close", () => {
3244
- clearInterval(pingTimer);
3245
- resolve13();
3246
- });
3247
- socket.addEventListener("error", () => {
3248
- clearInterval(pingTimer);
3249
- reject(new Error("Remote connector websocket failed."));
3250
- });
3568
+ };
3569
+
3570
+ // src/cli/commands/remote.ts
3571
+ function normalizeOptionalString5(value) {
3572
+ if (typeof value !== "string") {
3573
+ return void 0;
3574
+ }
3575
+ const trimmed = value.trim();
3576
+ return trimmed.length > 0 ? trimmed : void 0;
3577
+ }
3578
+ function resolveConfiguredLocalOrigin(config2) {
3579
+ const state = readServiceState();
3580
+ if (state && isProcessRunning(state.pid) && Number.isFinite(state.uiPort)) {
3581
+ return `http://127.0.0.1:${state.uiPort}`;
3582
+ }
3583
+ const port = typeof config2.ui.port === "number" && Number.isFinite(config2.ui.port) ? config2.ui.port : 18791;
3584
+ return `http://127.0.0.1:${port}`;
3585
+ }
3586
+ async function probeLocalUi(localOrigin) {
3587
+ try {
3588
+ const response = await fetch(`${localOrigin}/api/health`);
3589
+ if (!response.ok) {
3590
+ return { ok: false, detail: `health returned ${response.status}` };
3591
+ }
3592
+ return { ok: true, detail: "health endpoint returned ok" };
3593
+ } catch (error) {
3594
+ return {
3595
+ ok: false,
3596
+ detail: error instanceof Error ? error.message : String(error)
3597
+ };
3598
+ }
3599
+ }
3600
+ var RemoteCommands = class {
3601
+ enableConfig(opts = {}) {
3602
+ const config2 = loadConfig10(getConfigPath4());
3603
+ const next = {
3604
+ ...config2,
3605
+ remote: {
3606
+ ...config2.remote,
3607
+ enabled: true,
3608
+ ...normalizeOptionalString5(opts.apiBase) ? { platformApiBase: normalizeOptionalString5(opts.apiBase) ?? "" } : {},
3609
+ ...normalizeOptionalString5(opts.name) ? { deviceName: normalizeOptionalString5(opts.name) ?? "" } : {}
3610
+ }
3611
+ };
3612
+ saveConfig7(next);
3613
+ return {
3614
+ changed: config2.remote.enabled !== next.remote.enabled || config2.remote.platformApiBase !== next.remote.platformApiBase || config2.remote.deviceName !== next.remote.deviceName,
3615
+ config: next
3616
+ };
3617
+ }
3618
+ disableConfig() {
3619
+ const config2 = loadConfig10(getConfigPath4());
3620
+ const next = {
3621
+ ...config2,
3622
+ remote: {
3623
+ ...config2.remote,
3624
+ enabled: false
3625
+ }
3626
+ };
3627
+ saveConfig7(next);
3628
+ return {
3629
+ changed: config2.remote.enabled !== next.remote.enabled,
3630
+ config: next
3631
+ };
3632
+ }
3633
+ async connect(opts = {}) {
3634
+ const connector = new RemoteConnector();
3635
+ await connector.run({
3636
+ ...opts,
3637
+ mode: "foreground"
3251
3638
  });
3252
3639
  }
3253
- async connect(opts = {}) {
3254
- const { platformBase, token, config: config2 } = this.resolvePlatformAccess(opts);
3255
- const localOrigin = this.resolveLocalOrigin(config2, opts);
3256
- await this.ensureLocalUiHealthy(localOrigin);
3257
- const deviceInstallId = this.ensureDeviceInstallId();
3258
- const displayName = typeof opts.name === "string" && opts.name.trim().length > 0 ? opts.name.trim() : hostname();
3259
- const device = await this.registerDevice({
3260
- platformBase,
3261
- token,
3262
- deviceInstallId,
3263
- displayName,
3264
- localOrigin
3265
- });
3266
- console.log(`\u2713 Remote device registered: ${device.displayName} (${device.id})`);
3267
- console.log(`\u2713 Local origin: ${localOrigin}`);
3268
- console.log(`\u2713 Platform: ${platformBase}`);
3269
- const wsUrl = `${platformBase.replace(/^http/i, "ws")}/platform/remote/connect?deviceId=${encodeURIComponent(device.id)}&token=${encodeURIComponent(token)}`;
3270
- do {
3271
- try {
3272
- await this.connectOnce({ wsUrl, localOrigin });
3273
- } catch (error) {
3274
- console.error(`Remote connector error: ${error instanceof Error ? error.message : String(error)}`);
3275
- }
3276
- if (opts.once) {
3277
- break;
3640
+ async status(opts = {}) {
3641
+ const config2 = loadConfig10(getConfigPath4());
3642
+ const snapshot = resolveRemoteStatusSnapshot(config2);
3643
+ if (opts.json) {
3644
+ console.log(JSON.stringify(snapshot, null, 2));
3645
+ return;
3646
+ }
3647
+ const runtime2 = snapshot.runtime;
3648
+ console.log("NextClaw Remote Status");
3649
+ console.log(`Enabled: ${snapshot.configuredEnabled ? "yes" : "no"}`);
3650
+ console.log(`Mode: ${runtime2?.mode ?? "service"}`);
3651
+ console.log(`State: ${runtime2?.state ?? "disabled"}`);
3652
+ console.log(`Device: ${runtime2?.deviceName ?? normalizeOptionalString5(config2.remote.deviceName) ?? hostname3()}`);
3653
+ console.log(
3654
+ `Platform: ${runtime2?.platformBase ?? normalizeOptionalString5(config2.remote.platformApiBase) ?? normalizeOptionalString5(config2.providers.nextclaw?.apiBase) ?? "not set"}`
3655
+ );
3656
+ console.log(`Local origin: ${runtime2?.localOrigin ?? resolveConfiguredLocalOrigin(config2)}`);
3657
+ if (runtime2?.deviceId) {
3658
+ console.log(`Device ID: ${runtime2.deviceId}`);
3659
+ }
3660
+ if (runtime2?.lastConnectedAt) {
3661
+ console.log(`Last connected: ${runtime2.lastConnectedAt}`);
3662
+ }
3663
+ if (runtime2?.lastError) {
3664
+ console.log(`Last error: ${runtime2.lastError}`);
3665
+ }
3666
+ }
3667
+ async doctor(opts = {}) {
3668
+ const config2 = loadConfig10(getConfigPath4());
3669
+ const snapshot = resolveRemoteStatusSnapshot(config2);
3670
+ const localOrigin = resolveConfiguredLocalOrigin(config2);
3671
+ const localUi = await probeLocalUi(localOrigin);
3672
+ const token = normalizeOptionalString5(config2.providers.nextclaw?.apiKey);
3673
+ const platformApiBase = normalizeOptionalString5(config2.remote.platformApiBase) ?? normalizeOptionalString5(config2.providers.nextclaw?.apiBase);
3674
+ const checks = [
3675
+ {
3676
+ name: "remote-enabled",
3677
+ ok: snapshot.configuredEnabled,
3678
+ detail: snapshot.configuredEnabled ? "enabled in config" : "disabled in config"
3679
+ },
3680
+ {
3681
+ name: "platform-token",
3682
+ ok: Boolean(token),
3683
+ detail: token ? "token configured" : 'run "nextclaw login" first'
3684
+ },
3685
+ {
3686
+ name: "platform-api-base",
3687
+ ok: Boolean(platformApiBase),
3688
+ detail: platformApiBase ?? "set remote.platformApiBase or login with --api-base"
3689
+ },
3690
+ {
3691
+ name: "local-ui",
3692
+ ok: localUi.ok,
3693
+ detail: `${localOrigin} (${localUi.detail})`
3694
+ },
3695
+ {
3696
+ name: "service-runtime",
3697
+ ok: snapshot.runtime?.state === "connected",
3698
+ detail: snapshot.runtime ? snapshot.runtime.state : "no managed remote runtime detected"
3278
3699
  }
3279
- console.log("Remote connector disconnected. Reconnecting in 3s...");
3280
- await delay(3e3);
3281
- } while (!opts.once);
3700
+ ];
3701
+ if (opts.json) {
3702
+ console.log(JSON.stringify({ generatedAt: (/* @__PURE__ */ new Date()).toISOString(), checks, snapshot }, null, 2));
3703
+ return;
3704
+ }
3705
+ console.log("NextClaw Remote Doctor");
3706
+ for (const check of checks) {
3707
+ console.log(`${check.ok ? "\u2713" : "\u2717"} ${check.name}: ${check.detail}`);
3708
+ }
3282
3709
  }
3283
3710
  };
3284
3711
 
@@ -3287,14 +3714,100 @@ import { createServer as createNetServer } from "net";
3287
3714
  import { existsSync as existsSync7, readFileSync as readFileSync7 } from "fs";
3288
3715
  import { resolve as resolve8 } from "path";
3289
3716
  import {
3290
- APP_NAME,
3291
- getConfigPath as getConfigPath4,
3717
+ APP_NAME as APP_NAME2,
3718
+ getConfigPath as getConfigPath5,
3292
3719
  getDataDir as getDataDir5,
3293
3720
  getWorkspacePath as getWorkspacePath4,
3294
3721
  hasSecretRef,
3295
- loadConfig as loadConfig9
3722
+ loadConfig as loadConfig11
3296
3723
  } from "@nextclaw/core";
3297
3724
  import { listBuiltinProviders } from "@nextclaw/runtime";
3725
+
3726
+ // src/cli/commands/diagnostics-render.ts
3727
+ import { APP_NAME } from "@nextclaw/core";
3728
+ function printStatusReport(params) {
3729
+ const { logo, report, verbose } = params;
3730
+ console.log(`${logo} ${APP_NAME} Status`);
3731
+ console.log(`Level: ${report.level}`);
3732
+ console.log(`Generated: ${report.generatedAt}`);
3733
+ console.log("");
3734
+ printProcessSection(report);
3735
+ printEndpointSection(report);
3736
+ printProviderSection(report);
3737
+ printTextList("Fix actions", report.fixActions);
3738
+ printTextList("Issues", report.issues);
3739
+ printTextList("Recommendations", report.recommendations);
3740
+ if (verbose && report.logTail.length > 0) {
3741
+ console.log("");
3742
+ console.log("Recent logs:");
3743
+ for (const line of report.logTail) {
3744
+ console.log(line);
3745
+ }
3746
+ }
3747
+ }
3748
+ function printProcessSection(report) {
3749
+ const processLabel = report.process.running ? `running (PID ${report.process.pid})` : report.process.staleState ? "stale-state" : "stopped";
3750
+ console.log(`Process: ${processLabel}`);
3751
+ console.log(`State file: ${report.serviceStatePath} ${report.serviceStateExists ? "\u2713" : "\u2717"}`);
3752
+ if (report.process.startedAt) {
3753
+ console.log(`Started: ${report.process.startedAt}`);
3754
+ }
3755
+ console.log(`Managed health: ${report.health.managed.state} (${report.health.managed.detail})`);
3756
+ if (!report.process.running) {
3757
+ console.log(`Configured health: ${report.health.configured.state} (${report.health.configured.detail})`);
3758
+ }
3759
+ }
3760
+ function printEndpointSection(report) {
3761
+ console.log(`UI: ${report.endpoints.uiUrl ?? report.endpoints.configuredUiUrl}`);
3762
+ console.log(`API: ${report.endpoints.apiUrl ?? report.endpoints.configuredApiUrl}`);
3763
+ console.log(`Remote: ${report.remote.configuredEnabled ? "enabled" : "disabled"}${report.remote.runtime ? ` (${report.remote.runtime.state})` : ""}`);
3764
+ if (report.remote.runtime?.deviceName) {
3765
+ console.log(`Remote device: ${report.remote.runtime.deviceName}`);
3766
+ }
3767
+ if (report.remote.runtime?.platformBase) {
3768
+ console.log(`Remote platform: ${report.remote.runtime.platformBase}`);
3769
+ }
3770
+ if (report.remote.runtime?.lastError) {
3771
+ console.log(`Remote error: ${report.remote.runtime.lastError}`);
3772
+ }
3773
+ console.log(`Config: ${report.configPath} ${report.configExists ? "\u2713" : "\u2717"}`);
3774
+ console.log(`Workspace: ${report.workspacePath} ${report.workspaceExists ? "\u2713" : "\u2717"}`);
3775
+ console.log(`Model: ${report.model}`);
3776
+ }
3777
+ function printProviderSection(report) {
3778
+ for (const provider of report.providers) {
3779
+ console.log(`${provider.name}: ${provider.configured ? "\u2713" : "not set"}${provider.detail ? ` (${provider.detail})` : ""}`);
3780
+ }
3781
+ }
3782
+ function printDoctorReport(params) {
3783
+ console.log(`${params.logo} ${APP_NAME} Doctor`);
3784
+ console.log(`Generated: ${params.generatedAt}`);
3785
+ console.log("");
3786
+ for (const check of params.checks) {
3787
+ const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "!" : "\u2717";
3788
+ console.log(`${icon} ${check.name}: ${check.detail}`);
3789
+ }
3790
+ printTextList("Recommendations", params.recommendations);
3791
+ if (params.verbose && params.logTail.length > 0) {
3792
+ console.log("");
3793
+ console.log("Recent logs:");
3794
+ for (const line of params.logTail) {
3795
+ console.log(line);
3796
+ }
3797
+ }
3798
+ }
3799
+ function printTextList(title, items) {
3800
+ if (items.length === 0) {
3801
+ return;
3802
+ }
3803
+ console.log("");
3804
+ console.log(`${title}:`);
3805
+ for (const item of items) {
3806
+ console.log(`- ${item}`);
3807
+ }
3808
+ }
3809
+
3810
+ // src/cli/commands/diagnostics.ts
3298
3811
  var DiagnosticsCommands = class {
3299
3812
  constructor(deps) {
3300
3813
  this.deps = deps;
@@ -3309,56 +3822,7 @@ var DiagnosticsCommands = class {
3309
3822
  process.exitCode = 0;
3310
3823
  return;
3311
3824
  }
3312
- console.log(`${this.deps.logo} ${APP_NAME} Status`);
3313
- console.log(`Level: ${report.level}`);
3314
- console.log(`Generated: ${report.generatedAt}`);
3315
- console.log("");
3316
- const processLabel = report.process.running ? `running (PID ${report.process.pid})` : report.process.staleState ? "stale-state" : "stopped";
3317
- console.log(`Process: ${processLabel}`);
3318
- console.log(`State file: ${report.serviceStatePath} ${report.serviceStateExists ? "\u2713" : "\u2717"}`);
3319
- if (report.process.startedAt) {
3320
- console.log(`Started: ${report.process.startedAt}`);
3321
- }
3322
- console.log(`Managed health: ${report.health.managed.state} (${report.health.managed.detail})`);
3323
- if (!report.process.running) {
3324
- console.log(`Configured health: ${report.health.configured.state} (${report.health.configured.detail})`);
3325
- }
3326
- console.log(`UI: ${report.endpoints.uiUrl ?? report.endpoints.configuredUiUrl}`);
3327
- console.log(`API: ${report.endpoints.apiUrl ?? report.endpoints.configuredApiUrl}`);
3328
- console.log(`Config: ${report.configPath} ${report.configExists ? "\u2713" : "\u2717"}`);
3329
- console.log(`Workspace: ${report.workspacePath} ${report.workspaceExists ? "\u2713" : "\u2717"}`);
3330
- console.log(`Model: ${report.model}`);
3331
- for (const provider of report.providers) {
3332
- console.log(`${provider.name}: ${provider.configured ? "\u2713" : "not set"}${provider.detail ? ` (${provider.detail})` : ""}`);
3333
- }
3334
- if (report.fixActions.length > 0) {
3335
- console.log("");
3336
- console.log("Fix actions:");
3337
- for (const action of report.fixActions) {
3338
- console.log(`- ${action}`);
3339
- }
3340
- }
3341
- if (report.issues.length > 0) {
3342
- console.log("");
3343
- console.log("Issues:");
3344
- for (const issue of report.issues) {
3345
- console.log(`- ${issue}`);
3346
- }
3347
- }
3348
- if (report.recommendations.length > 0) {
3349
- console.log("");
3350
- console.log("Recommendations:");
3351
- for (const recommendation of report.recommendations) {
3352
- console.log(`- ${recommendation}`);
3353
- }
3354
- }
3355
- if (opts.verbose && report.logTail.length > 0) {
3356
- console.log("");
3357
- console.log("Recent logs:");
3358
- for (const line of report.logTail) {
3359
- console.log(line);
3360
- }
3361
- }
3825
+ printStatusReport({ logo: this.deps.logo, report, verbose: Boolean(opts.verbose) });
3362
3826
  process.exitCode = 0;
3363
3827
  }
3364
3828
  async doctor(opts = {}) {
@@ -3429,32 +3893,19 @@ var DiagnosticsCommands = class {
3429
3893
  process.exitCode = exitCode;
3430
3894
  return;
3431
3895
  }
3432
- console.log(`${this.deps.logo} ${APP_NAME} Doctor`);
3433
- console.log(`Generated: ${report.generatedAt}`);
3434
- console.log("");
3435
- for (const check of checks) {
3436
- const icon = check.status === "pass" ? "\u2713" : check.status === "warn" ? "!" : "\u2717";
3437
- console.log(`${icon} ${check.name}: ${check.detail}`);
3438
- }
3439
- if (report.recommendations.length > 0) {
3440
- console.log("");
3441
- console.log("Recommendations:");
3442
- for (const recommendation of report.recommendations) {
3443
- console.log(`- ${recommendation}`);
3444
- }
3445
- }
3446
- if (opts.verbose && report.logTail.length > 0) {
3447
- console.log("");
3448
- console.log("Recent logs:");
3449
- for (const line of report.logTail) {
3450
- console.log(line);
3451
- }
3452
- }
3896
+ printDoctorReport({
3897
+ logo: this.deps.logo,
3898
+ generatedAt: report.generatedAt,
3899
+ checks,
3900
+ recommendations: report.recommendations,
3901
+ verbose: Boolean(opts.verbose),
3902
+ logTail: report.logTail
3903
+ });
3453
3904
  process.exitCode = exitCode;
3454
3905
  }
3455
3906
  async collectRuntimeStatus(params) {
3456
- const configPath = getConfigPath4();
3457
- const config2 = loadConfig9();
3907
+ const configPath = getConfigPath5();
3908
+ const config2 = loadConfig11();
3458
3909
  const workspacePath = getWorkspacePath4(config2.agents.defaults.workspace);
3459
3910
  const serviceStatePath = resolve8(getDataDir5(), "run", "service.json");
3460
3911
  const fixActions = [];
@@ -3474,59 +3925,23 @@ var DiagnosticsCommands = class {
3474
3925
  const managedApiUrl = serviceState?.apiUrl ?? null;
3475
3926
  const managedHealth = running && managedApiUrl ? await this.probeApiHealth(`${managedApiUrl}/health`) : { state: "unreachable", detail: "service not running" };
3476
3927
  const configuredHealth = await this.probeApiHealth(`${configuredApiUrl}/health`, 900);
3928
+ const remote = resolveRemoteStatusSnapshot(config2);
3477
3929
  const orphanSuspected = !running && configuredHealth.state === "ok";
3478
- const providers = listBuiltinProviders().map((spec) => {
3479
- const provider = config2.providers[spec.name];
3480
- const apiKeyRefSet = hasSecretRef(config2, `providers.${spec.name}.apiKey`);
3481
- if (!provider) {
3482
- return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
3483
- }
3484
- if (spec.isLocal) {
3485
- return {
3486
- name: spec.displayName ?? spec.name,
3487
- configured: Boolean(provider.apiBase),
3488
- detail: provider.apiBase ? provider.apiBase : "apiBase not set"
3489
- };
3490
- }
3491
- return {
3492
- name: spec.displayName ?? spec.name,
3493
- configured: Boolean(provider.apiKey) || apiKeyRefSet,
3494
- detail: provider.apiKey ? "apiKey set" : apiKeyRefSet ? "apiKey ref set" : "apiKey not set"
3495
- };
3496
- });
3930
+ const providers = this.listProviderStatuses(config2);
3497
3931
  const issues = [];
3498
3932
  const recommendations = [];
3499
- if (!existsSync7(configPath)) {
3500
- issues.push("Config file is missing.");
3501
- recommendations.push(`Run ${APP_NAME} init to create config files.`);
3502
- }
3503
- if (!existsSync7(workspacePath)) {
3504
- issues.push("Workspace directory does not exist.");
3505
- recommendations.push(`Run ${APP_NAME} init to create workspace templates.`);
3506
- }
3507
- if (staleState) {
3508
- issues.push("Service state is stale (state exists but process is not running).");
3509
- recommendations.push(`Run ${APP_NAME} status --fix to clean stale state.`);
3510
- }
3511
- if (running && managedHealth.state !== "ok") {
3512
- issues.push(`Managed service health check failed: ${managedHealth.detail}`);
3513
- recommendations.push(`Check logs at ${serviceState?.logPath ?? resolveServiceLogPath()}.`);
3514
- }
3515
- if (running && serviceState?.startupState === "degraded" && managedHealth.state !== "ok") {
3516
- const startupHint = serviceState.startupLastProbeError ? ` (${serviceState.startupLastProbeError})` : "";
3517
- issues.push(`Service is in degraded startup state${startupHint}.`);
3518
- recommendations.push(`Wait and re-check ${APP_NAME} status; if it does not recover, inspect logs and restart.`);
3519
- }
3520
- if (!running) {
3521
- recommendations.push(`Run ${APP_NAME} start to launch the service.`);
3522
- }
3523
- if (orphanSuspected) {
3524
- issues.push("A service appears healthy on configured API endpoint, but state is missing/stale.");
3525
- recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
3526
- }
3527
- if (!providers.some((provider) => provider.configured)) {
3528
- recommendations.push("Configure at least one provider API key in UI or config before expecting agent replies.");
3529
- }
3933
+ this.collectRuntimeIssues({
3934
+ configPath,
3935
+ workspacePath,
3936
+ staleState,
3937
+ running,
3938
+ managedHealth,
3939
+ serviceState,
3940
+ orphanSuspected,
3941
+ providers,
3942
+ issues,
3943
+ recommendations
3944
+ });
3530
3945
  const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveServiceLogPath(), 25) : [];
3531
3946
  const level = running ? managedHealth.state === "ok" ? issues.length > 0 ? "degraded" : "healthy" : "degraded" : "stopped";
3532
3947
  const exitCode = 0;
@@ -3562,6 +3977,7 @@ var DiagnosticsCommands = class {
3562
3977
  issues,
3563
3978
  recommendations,
3564
3979
  logTail,
3980
+ remote,
3565
3981
  level,
3566
3982
  exitCode
3567
3983
  };
@@ -3588,6 +4004,60 @@ var DiagnosticsCommands = class {
3588
4004
  clearTimeout(timer);
3589
4005
  }
3590
4006
  }
4007
+ listProviderStatuses(config2) {
4008
+ return listBuiltinProviders().map((spec) => {
4009
+ const provider = config2.providers[spec.name];
4010
+ const apiKeyRefSet = hasSecretRef(config2, `providers.${spec.name}.apiKey`);
4011
+ if (!provider) {
4012
+ return { name: spec.displayName ?? spec.name, configured: false, detail: "missing config" };
4013
+ }
4014
+ if (spec.isLocal) {
4015
+ return {
4016
+ name: spec.displayName ?? spec.name,
4017
+ configured: Boolean(provider.apiBase),
4018
+ detail: provider.apiBase ? provider.apiBase : "apiBase not set"
4019
+ };
4020
+ }
4021
+ return {
4022
+ name: spec.displayName ?? spec.name,
4023
+ configured: Boolean(provider.apiKey) || apiKeyRefSet,
4024
+ detail: provider.apiKey ? "apiKey set" : apiKeyRefSet ? "apiKey ref set" : "apiKey not set"
4025
+ };
4026
+ });
4027
+ }
4028
+ collectRuntimeIssues(params) {
4029
+ if (!existsSync7(params.configPath)) {
4030
+ params.issues.push("Config file is missing.");
4031
+ params.recommendations.push(`Run ${APP_NAME2} init to create config files.`);
4032
+ }
4033
+ if (!existsSync7(params.workspacePath)) {
4034
+ params.issues.push("Workspace directory does not exist.");
4035
+ params.recommendations.push(`Run ${APP_NAME2} init to create workspace templates.`);
4036
+ }
4037
+ if (params.staleState) {
4038
+ params.issues.push("Service state is stale (state exists but process is not running).");
4039
+ params.recommendations.push(`Run ${APP_NAME2} status --fix to clean stale state.`);
4040
+ }
4041
+ if (params.running && params.managedHealth.state !== "ok") {
4042
+ params.issues.push(`Managed service health check failed: ${params.managedHealth.detail}`);
4043
+ params.recommendations.push(`Check logs at ${params.serviceState?.logPath ?? resolveServiceLogPath()}.`);
4044
+ }
4045
+ if (params.running && params.serviceState?.startupState === "degraded" && params.managedHealth.state !== "ok") {
4046
+ const startupHint = params.serviceState.startupLastProbeError ? ` (${params.serviceState.startupLastProbeError})` : "";
4047
+ params.issues.push(`Service is in degraded startup state${startupHint}.`);
4048
+ params.recommendations.push(`Wait and re-check ${APP_NAME2} status; if it does not recover, inspect logs and restart.`);
4049
+ }
4050
+ if (!params.running) {
4051
+ params.recommendations.push(`Run ${APP_NAME2} start to launch the service.`);
4052
+ }
4053
+ if (params.orphanSuspected) {
4054
+ params.issues.push("A service appears healthy on configured API endpoint, but state is missing/stale.");
4055
+ params.recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
4056
+ }
4057
+ if (!params.providers.some((provider) => provider.configured)) {
4058
+ params.recommendations.push("Configure at least one provider API key in UI or config before expecting agent replies.");
4059
+ }
4060
+ }
3591
4061
  readLogTail(path2, maxLines = 25) {
3592
4062
  if (!existsSync7(path2)) {
3593
4063
  return [];
@@ -3651,8 +4121,8 @@ import {
3651
4121
  redactConfigObject
3652
4122
  } from "@nextclaw/core";
3653
4123
  var hashRaw = (raw) => createHash("sha256").update(raw).digest("hex");
3654
- var readConfigSnapshot = (getConfigPath7) => {
3655
- const path2 = getConfigPath7();
4124
+ var readConfigSnapshot = (getConfigPath8) => {
4125
+ const path2 = getConfigPath8();
3656
4126
  let raw = "";
3657
4127
  let parsed = {};
3658
4128
  if (existsSync8(path2)) {
@@ -4113,7 +4583,7 @@ var MissingProvider = class extends LLMProvider {
4113
4583
  };
4114
4584
 
4115
4585
  // src/cli/commands/service-marketplace-installer.ts
4116
- import { getWorkspacePath as getWorkspacePath5, loadConfig as loadConfig11 } from "@nextclaw/core";
4586
+ import { getWorkspacePath as getWorkspacePath5, loadConfig as loadConfig13 } from "@nextclaw/core";
4117
4587
  import { existsSync as existsSync9, rmSync as rmSync4 } from "fs";
4118
4588
  import { join as join5 } from "path";
4119
4589
 
@@ -4163,7 +4633,7 @@ var buildMarketplaceSkillInstallArgs = (params) => {
4163
4633
  };
4164
4634
 
4165
4635
  // src/cli/commands/service-mcp-marketplace-ops.ts
4166
- import { loadConfig as loadConfig10, saveConfig as saveConfig7 } from "@nextclaw/core";
4636
+ import { loadConfig as loadConfig12, saveConfig as saveConfig8 } from "@nextclaw/core";
4167
4637
  import { McpDoctorFacade as McpDoctorFacade2, McpMutationService as McpMutationService2 } from "@nextclaw/mcp";
4168
4638
  var ServiceMcpMarketplaceOps = class {
4169
4639
  constructor(options) {
@@ -4231,13 +4701,13 @@ var ServiceMcpMarketplaceOps = class {
4231
4701
  }
4232
4702
  createMutationService() {
4233
4703
  return new McpMutationService2({
4234
- getConfig: () => loadConfig10(),
4235
- saveConfig: (config2) => saveConfig7(config2)
4704
+ getConfig: () => loadConfig12(),
4705
+ saveConfig: (config2) => saveConfig8(config2)
4236
4706
  });
4237
4707
  }
4238
4708
  createDoctorFacade() {
4239
4709
  return new McpDoctorFacade2({
4240
- getConfig: () => loadConfig10()
4710
+ getConfig: () => loadConfig12()
4241
4711
  });
4242
4712
  }
4243
4713
  };
@@ -4278,7 +4748,7 @@ var ServiceMarketplaceInstaller = class {
4278
4748
  if (params.kind && params.kind !== "marketplace") {
4279
4749
  throw new Error(`Unsupported marketplace skill kind: ${params.kind}`);
4280
4750
  }
4281
- const workspace = getWorkspacePath5(loadConfig11().agents.defaults.workspace);
4751
+ const workspace = getWorkspacePath5(loadConfig13().agents.defaults.workspace);
4282
4752
  const args = buildMarketplaceSkillInstallArgs({
4283
4753
  slug: params.slug,
4284
4754
  workspace,
@@ -4317,7 +4787,7 @@ var ServiceMarketplaceInstaller = class {
4317
4787
  return { message: result.message };
4318
4788
  }
4319
4789
  async uninstallSkill(slug) {
4320
- const workspace = getWorkspacePath5(loadConfig11().agents.defaults.workspace);
4790
+ const workspace = getWorkspacePath5(loadConfig13().agents.defaults.workspace);
4321
4791
  const targetDir = join5(workspace, "skills", slug);
4322
4792
  if (!existsSync9(targetDir)) {
4323
4793
  throw new Error(`Skill not installed in workspace: ${slug}`);
@@ -4444,6 +4914,35 @@ async function reloadServicePlugins(params) {
4444
4914
  };
4445
4915
  }
4446
4916
 
4917
+ // src/cli/commands/service-startup-support.ts
4918
+ var pluginGatewayLogger = {
4919
+ info: (message) => console.log(`[plugins] ${message}`),
4920
+ warn: (message) => console.warn(`[plugins] ${message}`),
4921
+ error: (message) => console.error(`[plugins] ${message}`),
4922
+ debug: (message) => console.debug(`[plugins] ${message}`)
4923
+ };
4924
+ function logPluginGatewayDiagnostics(diagnostics) {
4925
+ for (const diag of diagnostics) {
4926
+ const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
4927
+ const text = `${prefix}${diag.message}`;
4928
+ if (diag.level === "error") {
4929
+ console.error(`[plugins] ${text}`);
4930
+ } else {
4931
+ console.warn(`[plugins] ${text}`);
4932
+ }
4933
+ }
4934
+ }
4935
+ async function startGatewaySupportServices(params) {
4936
+ if (params.cronJobs > 0) {
4937
+ console.log(`\u2713 Cron: ${params.cronJobs} scheduled jobs`);
4938
+ }
4939
+ console.log("\u2713 Heartbeat: every 30m");
4940
+ params.remoteModule?.start();
4941
+ params.watchConfigFile();
4942
+ await params.startCron();
4943
+ await params.startHeartbeat();
4944
+ }
4945
+
4447
4946
  // src/cli/commands/agent-runtime-pool.ts
4448
4947
  import {
4449
4948
  NativeAgentEngine,
@@ -4941,14 +5440,14 @@ function formatUserFacingError(error, maxChars = 320) {
4941
5440
  // src/cli/commands/cli-subcommand-launch.ts
4942
5441
  import { createRequire } from "module";
4943
5442
  import { extname, resolve as resolve9 } from "path";
4944
- import { fileURLToPath as fileURLToPath2 } from "url";
5443
+ import { fileURLToPath as fileURLToPath3 } from "url";
4945
5444
  var require2 = createRequire(import.meta.url);
4946
5445
  var resolveCliSubcommandEntry = (params) => {
4947
5446
  const argvEntry = params.argvEntry?.trim();
4948
5447
  if (argvEntry) {
4949
5448
  return resolve9(argvEntry);
4950
5449
  }
4951
- return fileURLToPath2(new URL("../index.js", params.importMetaUrl));
5450
+ return fileURLToPath3(new URL("../index.js", params.importMetaUrl));
4952
5451
  };
4953
5452
 
4954
5453
  // src/cli/commands/ncp/create-ui-ncp-agent.ts
@@ -5423,7 +5922,7 @@ function resolveRequestedToolNames(metadata) {
5423
5922
  )
5424
5923
  );
5425
5924
  }
5426
- function normalizeOptionalString3(value) {
5925
+ function normalizeOptionalString6(value) {
5427
5926
  return normalizeString(value) ?? void 0;
5428
5927
  }
5429
5928
  function readMetadataModel(metadata) {
@@ -5543,7 +6042,7 @@ var NextclawNcpContextBuilder = class {
5543
6042
  if (inboundModel) {
5544
6043
  session.metadata.preferred_model = inboundModel;
5545
6044
  }
5546
- const effectiveModel = normalizeOptionalString3(session.metadata.preferred_model) ?? profile.model;
6045
+ const effectiveModel = normalizeOptionalString6(session.metadata.preferred_model) ?? profile.model;
5547
6046
  const clearThinking = requestMetadata.clear_thinking === true || requestMetadata.reset_thinking === true;
5548
6047
  if (clearThinking) {
5549
6048
  delete session.metadata.preferred_thinking;
@@ -5560,8 +6059,8 @@ var NextclawNcpContextBuilder = class {
5560
6059
  model: effectiveModel,
5561
6060
  sessionThinkingLevel: parseThinkingLevel(session.metadata.preferred_thinking) ?? null
5562
6061
  });
5563
- const channel = normalizeOptionalString3(requestMetadata.channel) ?? normalizeOptionalString3(session.metadata.last_channel) ?? "ui";
5564
- const chatId = normalizeOptionalString3(requestMetadata.chatId) ?? normalizeOptionalString3(requestMetadata.chat_id) ?? normalizeOptionalString3(session.metadata.last_to) ?? "web-ui";
6062
+ const channel = normalizeOptionalString6(requestMetadata.channel) ?? normalizeOptionalString6(session.metadata.last_channel) ?? "ui";
6063
+ const chatId = normalizeOptionalString6(requestMetadata.chatId) ?? normalizeOptionalString6(requestMetadata.chat_id) ?? normalizeOptionalString6(session.metadata.last_to) ?? "web-ui";
5565
6064
  session.metadata.last_channel = channel;
5566
6065
  session.metadata.last_to = chatId;
5567
6066
  const requestedSkillNames = resolveRequestedSkillNames(requestMetadata);
@@ -6279,6 +6778,119 @@ async function createUiNcpAgent(params) {
6279
6778
  };
6280
6779
  }
6281
6780
 
6781
+ // src/cli/remote/remote-service-module.ts
6782
+ var RemoteServiceModule = class {
6783
+ constructor(deps) {
6784
+ this.deps = deps;
6785
+ }
6786
+ abortController = null;
6787
+ runTask = null;
6788
+ statusStore = new RemoteStatusStore("service");
6789
+ start() {
6790
+ if (!this.deps.config.remote.enabled) {
6791
+ this.statusStore.write({
6792
+ enabled: false,
6793
+ state: "disabled",
6794
+ deviceName: void 0,
6795
+ deviceId: void 0,
6796
+ platformBase: void 0,
6797
+ localOrigin: this.deps.localOrigin,
6798
+ lastError: null,
6799
+ lastConnectedAt: null
6800
+ });
6801
+ return null;
6802
+ }
6803
+ const logger = this.deps.logger ?? {
6804
+ info: (message) => console.log(`[remote] ${message}`),
6805
+ warn: (message) => console.warn(`[remote] ${message}`),
6806
+ error: (message) => console.error(`[remote] ${message}`)
6807
+ };
6808
+ this.abortController = new AbortController();
6809
+ const connector = new RemoteConnector(logger);
6810
+ this.runTask = connector.run({
6811
+ mode: "service",
6812
+ signal: this.abortController.signal,
6813
+ autoReconnect: this.deps.config.remote.autoReconnect,
6814
+ localOrigin: this.deps.localOrigin,
6815
+ statusStore: this.statusStore
6816
+ });
6817
+ void this.runTask.catch((error) => {
6818
+ const message = error instanceof Error ? error.message : String(error);
6819
+ this.statusStore.write({
6820
+ enabled: true,
6821
+ state: "error",
6822
+ deviceName: this.deps.config.remote.deviceName || void 0,
6823
+ deviceId: void 0,
6824
+ platformBase: this.deps.config.remote.platformApiBase || void 0,
6825
+ localOrigin: this.deps.localOrigin,
6826
+ lastError: message
6827
+ });
6828
+ logger.error(message);
6829
+ });
6830
+ return this.runTask;
6831
+ }
6832
+ async stop() {
6833
+ this.abortController?.abort();
6834
+ try {
6835
+ await this.runTask;
6836
+ } catch {
6837
+ } finally {
6838
+ this.abortController = null;
6839
+ this.runTask = null;
6840
+ }
6841
+ }
6842
+ };
6843
+
6844
+ // src/cli/commands/service-remote-runtime.ts
6845
+ function createManagedRemoteModule(params) {
6846
+ if (!params.config.ui.enabled) {
6847
+ return null;
6848
+ }
6849
+ return new RemoteServiceModule({
6850
+ config: params.config,
6851
+ localOrigin: params.localOrigin,
6852
+ logger: {
6853
+ info: (message) => console.log(`[remote] ${message}`),
6854
+ warn: (message) => console.warn(`[remote] ${message}`),
6855
+ error: (message) => console.error(`[remote] ${message}`)
6856
+ }
6857
+ });
6858
+ }
6859
+ function writeInitialManagedServiceState(params) {
6860
+ writeServiceState({
6861
+ pid: params.snapshot.pid,
6862
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
6863
+ uiUrl: params.snapshot.uiUrl,
6864
+ apiUrl: params.snapshot.apiUrl,
6865
+ uiHost: params.snapshot.uiHost,
6866
+ uiPort: params.snapshot.uiPort,
6867
+ logPath: params.snapshot.logPath,
6868
+ startupLastProbeError: null,
6869
+ startupTimeoutMs: params.readinessTimeoutMs,
6870
+ startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
6871
+ ...params.config.remote.enabled ? { remote: buildConfiguredRemoteState(params.config) } : {}
6872
+ });
6873
+ }
6874
+ function writeReadyManagedServiceState(params) {
6875
+ const currentState = readServiceState();
6876
+ const state = {
6877
+ pid: params.snapshot.pid,
6878
+ startedAt: currentState?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
6879
+ uiUrl: params.snapshot.uiUrl,
6880
+ apiUrl: params.snapshot.apiUrl,
6881
+ uiHost: params.snapshot.uiHost,
6882
+ uiPort: params.snapshot.uiPort,
6883
+ logPath: params.snapshot.logPath,
6884
+ startupState: params.readiness.ready ? "ready" : "degraded",
6885
+ startupLastProbeError: params.readiness.lastProbeError,
6886
+ startupTimeoutMs: params.readinessTimeoutMs,
6887
+ startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
6888
+ ...currentState?.remote ? { remote: currentState.remote } : {}
6889
+ };
6890
+ writeServiceState(state);
6891
+ return state;
6892
+ }
6893
+
6282
6894
  // src/cli/commands/ui-chat-run-coordinator.ts
6283
6895
  import { existsSync as existsSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync9, writeFileSync as writeFileSync5 } from "fs";
6284
6896
  import { join as join6 } from "path";
@@ -6899,22 +7511,22 @@ var UiChatRunCoordinator = class {
6899
7511
 
6900
7512
  // src/cli/commands/service.ts
6901
7513
  var {
6902
- APP_NAME: APP_NAME2,
7514
+ APP_NAME: APP_NAME3,
6903
7515
  ChannelManager: ChannelManager2,
6904
7516
  CronService: CronService2,
6905
7517
  getApiBase,
6906
- getConfigPath: getConfigPath5,
7518
+ getConfigPath: getConfigPath6,
6907
7519
  getDataDir: getDataDir7,
6908
7520
  getProvider,
6909
7521
  getProviderName,
6910
7522
  getWorkspacePath: getWorkspacePath9,
6911
7523
  HeartbeatService,
6912
7524
  LiteLLMProvider,
6913
- loadConfig: loadConfig12,
7525
+ loadConfig: loadConfig14,
6914
7526
  MessageBus,
6915
7527
  ProviderManager,
6916
7528
  resolveConfigSecrets: resolveConfigSecrets2,
6917
- saveConfig: saveConfig8,
7529
+ saveConfig: saveConfig9,
6918
7530
  SessionManager,
6919
7531
  parseAgentScopedSessionKey: parseAgentScopedSessionKey3
6920
7532
  } = NextclawCore;
@@ -6934,8 +7546,8 @@ var ServiceCommands = class {
6934
7546
  async startGateway(options = {}) {
6935
7547
  this.applyLiveConfigReload = null;
6936
7548
  this.liveUiNcpAgent = null;
6937
- const runtimeConfigPath = getConfigPath5();
6938
- const config2 = resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath });
7549
+ const runtimeConfigPath = getConfigPath6();
7550
+ const config2 = resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath });
6939
7551
  const workspace = getWorkspacePath9(config2.agents.defaults.workspace);
6940
7552
  let pluginRegistry = loadPluginRegistry(config2, workspace);
6941
7553
  let extensionRegistry = toExtensionRegistry(pluginRegistry);
@@ -6948,27 +7560,11 @@ var ServiceCommands = class {
6948
7560
  });
6949
7561
  const sessionManager = new SessionManager(workspace);
6950
7562
  let pluginGatewayHandles = [];
6951
- const pluginGatewayLogger = {
6952
- info: (message) => console.log(`[plugins] ${message}`),
6953
- warn: (message) => console.warn(`[plugins] ${message}`),
6954
- error: (message) => console.error(`[plugins] ${message}`),
6955
- debug: (message) => console.debug(`[plugins] ${message}`)
6956
- };
6957
- const logPluginGatewayDiagnostics = (diagnostics) => {
6958
- for (const diag of diagnostics) {
6959
- const prefix = diag.pluginId ? `${diag.pluginId}: ` : "";
6960
- const text = `${prefix}${diag.message}`;
6961
- if (diag.level === "error") {
6962
- console.error(`[plugins] ${text}`);
6963
- } else {
6964
- console.warn(`[plugins] ${text}`);
6965
- }
6966
- }
6967
- };
6968
7563
  const cronStorePath = join7(getDataDir7(), "cron", "jobs.json");
6969
7564
  const cron2 = new CronService2(cronStorePath);
6970
7565
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
6971
7566
  const uiStaticDir = options.uiStaticDir === void 0 ? resolveUiStaticDir() : options.uiStaticDir;
7567
+ const localOrigin = resolveUiApiBase(uiConfig.host, uiConfig.port);
6972
7568
  if (!provider) {
6973
7569
  console.warn("Warning: No API key configured. The gateway is running, but agent replies are disabled until provider config is set.");
6974
7570
  }
@@ -6980,7 +7576,7 @@ var ServiceCommands = class {
6980
7576
  sessionManager,
6981
7577
  providerManager,
6982
7578
  makeProvider: (nextConfig) => this.makeProvider(nextConfig, { allowMissing: true }) ?? this.makeMissingProvider(nextConfig),
6983
- loadConfig: () => resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath }),
7579
+ loadConfig: () => resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath }),
6984
7580
  getExtensionChannels: () => extensionRegistry.channels,
6985
7581
  onRestartRequired: (paths) => {
6986
7582
  void this.deps.requestRestart({
@@ -6991,14 +7587,14 @@ var ServiceCommands = class {
6991
7587
  }
6992
7588
  });
6993
7589
  this.applyLiveConfigReload = async () => {
6994
- await reloader.applyReloadPlan(resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath }));
7590
+ await reloader.applyReloadPlan(resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath }));
6995
7591
  };
6996
7592
  const gatewayController = new GatewayControllerImpl({
6997
7593
  reloader,
6998
7594
  cron: cron2,
6999
7595
  sessionManager,
7000
- getConfigPath: getConfigPath5,
7001
- saveConfig: saveConfig8,
7596
+ getConfigPath: getConfigPath6,
7597
+ saveConfig: saveConfig9,
7002
7598
  requestRestart: async (options2) => {
7003
7599
  await this.deps.requestRestart({
7004
7600
  reason: options2?.reason ?? "gateway tool restart",
@@ -7024,7 +7620,7 @@ var ServiceCommands = class {
7024
7620
  resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
7025
7621
  registry: pluginRegistry,
7026
7622
  channel,
7027
- cfg: resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath }),
7623
+ cfg: resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath }),
7028
7624
  accountId
7029
7625
  })
7030
7626
  });
@@ -7057,14 +7653,14 @@ var ServiceCommands = class {
7057
7653
  });
7058
7654
  let pluginChannelBindings = getPluginChannelBindings3(pluginRegistry);
7059
7655
  setPluginRuntimeBridge({
7060
- loadConfig: () => toPluginConfigView(resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath }), pluginChannelBindings),
7656
+ loadConfig: () => toPluginConfigView(resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath }), pluginChannelBindings),
7061
7657
  writeConfigFile: async (nextConfigView) => {
7062
7658
  if (!nextConfigView || typeof nextConfigView !== "object" || Array.isArray(nextConfigView)) {
7063
7659
  throw new Error("plugin runtime writeConfigFile expects an object config");
7064
7660
  }
7065
- const current = loadConfig12();
7661
+ const current = loadConfig14();
7066
7662
  const next = mergePluginConfigView(current, nextConfigView, pluginChannelBindings);
7067
- saveConfig8(next);
7663
+ saveConfig9(next);
7068
7664
  },
7069
7665
  dispatchReplyWithBufferedBlockDispatcher: async ({ ctx, dispatcherOptions }) => {
7070
7666
  const bodyForAgent = typeof ctx.BodyForAgent === "string" ? ctx.BodyForAgent : "";
@@ -7138,23 +7734,23 @@ var ServiceCommands = class {
7138
7734
  providerManager,
7139
7735
  bus,
7140
7736
  gatewayController,
7141
- () => resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath }),
7737
+ () => resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath }),
7142
7738
  () => extensionRegistry,
7143
7739
  ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
7144
7740
  registry: pluginRegistry,
7145
7741
  channel,
7146
- cfg: resolveConfigSecrets2(loadConfig12(), { configPath: runtimeConfigPath }),
7742
+ cfg: resolveConfigSecrets2(loadConfig14(), { configPath: runtimeConfigPath }),
7147
7743
  accountId
7148
7744
  })
7149
7745
  );
7150
- const cronStatus = cron2.status();
7151
- if (cronStatus.jobs > 0) {
7152
- console.log(`\u2713 Cron: ${cronStatus.jobs} scheduled jobs`);
7153
- }
7154
- console.log("\u2713 Heartbeat: every 30m");
7155
- this.watchConfigFile(reloader);
7156
- await cron2.start();
7157
- await heartbeat.start();
7746
+ const remoteModule = createManagedRemoteModule({ config: config2, localOrigin });
7747
+ await startGatewaySupportServices({
7748
+ cronJobs: cron2.status().jobs,
7749
+ remoteModule,
7750
+ watchConfigFile: () => this.watchConfigFile(reloader),
7751
+ startCron: () => cron2.start(),
7752
+ startHeartbeat: () => heartbeat.start()
7753
+ });
7158
7754
  try {
7159
7755
  const startedPluginGateways = await startPluginChannelGateways2({
7160
7756
  registry: pluginRegistry,
@@ -7168,6 +7764,7 @@ var ServiceCommands = class {
7168
7764
  } finally {
7169
7765
  this.applyLiveConfigReload = null;
7170
7766
  this.liveUiNcpAgent = null;
7767
+ await remoteModule?.stop();
7171
7768
  await stopPluginChannelGateways2(pluginGatewayHandles);
7172
7769
  setPluginRuntimeBridge(null);
7173
7770
  }
@@ -7180,7 +7777,7 @@ var ServiceCommands = class {
7180
7777
  return trimmed || void 0;
7181
7778
  }
7182
7779
  watchConfigFile(reloader) {
7183
- const configPath = resolve10(getConfigPath5());
7780
+ const configPath = resolve10(getConfigPath6());
7184
7781
  const watcher = chokidar.watch(configPath, {
7185
7782
  ignoreInitial: true,
7186
7783
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
@@ -7301,7 +7898,7 @@ var ServiceCommands = class {
7301
7898
  });
7302
7899
  }
7303
7900
  async runForeground(options) {
7304
- const config2 = loadConfig12();
7901
+ const config2 = loadConfig14();
7305
7902
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
7306
7903
  const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
7307
7904
  if (options.open) {
@@ -7314,14 +7911,14 @@ var ServiceCommands = class {
7314
7911
  });
7315
7912
  }
7316
7913
  async startService(options) {
7317
- const config2 = loadConfig12();
7914
+ const config2 = loadConfig14();
7318
7915
  const uiConfig = resolveUiConfig(config2, options.uiOverrides);
7319
7916
  const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
7320
7917
  const apiUrl = `${uiUrl}/api`;
7321
7918
  const staticDir = resolveUiStaticDir();
7322
7919
  const existing = readServiceState();
7323
7920
  if (existing && isProcessRunning(existing.pid)) {
7324
- console.log(`\u2713 ${APP_NAME2} is already running (PID ${existing.pid})`);
7921
+ console.log(`\u2713 ${APP_NAME3} is already running (PID ${existing.pid})`);
7325
7922
  console.log(`UI: ${existing.uiUrl}`);
7326
7923
  console.log(`API: ${existing.apiUrl}`);
7327
7924
  const parsedUi = (() => {
@@ -7369,7 +7966,7 @@ var ServiceCommands = class {
7369
7966
  healthUrl
7370
7967
  });
7371
7968
  if (!portPreflight.ok) {
7372
- console.error(`Error: Cannot start ${APP_NAME2} because UI port ${uiConfig.port} is already occupied.`);
7969
+ console.error(`Error: Cannot start ${APP_NAME3} because UI port ${uiConfig.port} is already occupied.`);
7373
7970
  console.error(portPreflight.message);
7374
7971
  return;
7375
7972
  }
@@ -7384,10 +7981,8 @@ var ServiceCommands = class {
7384
7981
  logPath,
7385
7982
  `start requested: ui=${uiConfig.host}:${uiConfig.port}, readinessTimeoutMs=${readinessTimeoutMs}`
7386
7983
  );
7387
- console.log(`Starting ${APP_NAME2} background service (readiness timeout ${Math.ceil(readinessTimeoutMs / 1e3)}s)...`);
7388
- const serveArgs = buildServeArgs({
7389
- uiPort: uiConfig.port
7390
- });
7984
+ console.log(`Starting ${APP_NAME3} background service (readiness timeout ${Math.ceil(readinessTimeoutMs / 1e3)}s)...`);
7985
+ const serveArgs = buildServeArgs({ uiPort: uiConfig.port });
7391
7986
  this.appendStartupStage(logPath, `spawning background process: ${process.execPath} ${[...process.execArgv, ...serveArgs].join(" ")}`);
7392
7987
  const child = spawn2(process.execPath, [...process.execArgv, ...serveArgs], {
7393
7988
  env: process.env,
@@ -7408,6 +8003,11 @@ var ServiceCommands = class {
7408
8003
  });
7409
8004
  return;
7410
8005
  }
8006
+ writeInitialManagedServiceState({
8007
+ config: config2,
8008
+ readinessTimeoutMs,
8009
+ snapshot: { pid: child.pid, uiUrl, apiUrl, uiHost: uiConfig.host, uiPort: uiConfig.port, logPath }
8010
+ });
7411
8011
  this.appendStartupStage(logPath, `health probe started: ${healthUrl} (phase=quick, timeoutMs=${quickPhaseTimeoutMs})`);
7412
8012
  let readiness = await this.waitForBackgroundServiceReady({
7413
8013
  pid: child.pid,
@@ -7449,28 +8049,19 @@ var ServiceCommands = class {
7449
8049
  );
7450
8050
  }
7451
8051
  child.unref();
7452
- const state = {
7453
- pid: child.pid,
7454
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7455
- uiUrl,
7456
- apiUrl,
7457
- uiHost: uiConfig.host,
7458
- uiPort: uiConfig.port,
7459
- logPath,
7460
- startupState: readiness.ready ? "ready" : "degraded",
7461
- startupLastProbeError: readiness.lastProbeError,
7462
- startupTimeoutMs: readinessTimeoutMs,
7463
- startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString()
7464
- };
7465
- writeServiceState(state);
8052
+ const state = writeReadyManagedServiceState({
8053
+ readinessTimeoutMs,
8054
+ readiness,
8055
+ snapshot: { pid: child.pid, uiUrl, apiUrl, uiHost: uiConfig.host, uiPort: uiConfig.port, logPath }
8056
+ });
7466
8057
  if (!readiness.ready) {
7467
8058
  const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
7468
8059
  console.warn(
7469
- `Warning: ${APP_NAME2} is running (PID ${state.pid}) but not healthy yet after ${Math.ceil(readinessTimeoutMs / 1e3)}s. Marked as degraded.${hint}`
8060
+ `Warning: ${APP_NAME3} is running (PID ${state.pid}) but not healthy yet after ${Math.ceil(readinessTimeoutMs / 1e3)}s. Marked as degraded.${hint}`
7470
8061
  );
7471
- console.warn(`Tip: Run "${APP_NAME2} status --json" and check logs: ${logPath}`);
8062
+ console.warn(`Tip: Run "${APP_NAME3} status --json" and check logs: ${logPath}`);
7472
8063
  } else {
7473
- console.log(`\u2713 ${APP_NAME2} started in background (PID ${state.pid})`);
8064
+ console.log(`\u2713 ${APP_NAME3} started in background (PID ${state.pid})`);
7474
8065
  }
7475
8066
  console.log(`UI: ${uiUrl}`);
7476
8067
  console.log(`API: ${apiUrl}`);
@@ -7492,7 +8083,7 @@ var ServiceCommands = class {
7492
8083
  clearServiceState();
7493
8084
  return;
7494
8085
  }
7495
- console.log(`Stopping ${APP_NAME2} (PID ${state.pid})...`);
8086
+ console.log(`Stopping ${APP_NAME3} (PID ${state.pid})...`);
7496
8087
  try {
7497
8088
  process.kill(state.pid, "SIGTERM");
7498
8089
  } catch (error) {
@@ -7510,7 +8101,7 @@ var ServiceCommands = class {
7510
8101
  await waitForExit(state.pid, 2e3);
7511
8102
  }
7512
8103
  clearServiceState();
7513
- console.log(`\u2713 ${APP_NAME2} stopped`);
8104
+ console.log(`\u2713 ${APP_NAME3} stopped`);
7514
8105
  }
7515
8106
  async waitForBackgroundServiceReady(params) {
7516
8107
  const startedAt = Date.now();
@@ -7728,7 +8319,7 @@ var ServiceCommands = class {
7728
8319
  return null;
7729
8320
  }
7730
8321
  console.error("Error: No API key configured.");
7731
- console.error(`Set one in ${getConfigPath5()} under providers section`);
8322
+ console.error(`Set one in ${getConfigPath6()} under providers section`);
7732
8323
  process.exit(1);
7733
8324
  }
7734
8325
  return new LiteLLMProvider({
@@ -7765,8 +8356,8 @@ var ServiceCommands = class {
7765
8356
  }
7766
8357
  printServiceControlHints() {
7767
8358
  console.log("Service controls:");
7768
- console.log(` - Check status: ${APP_NAME2} status`);
7769
- console.log(` - If you need to stop the service, run: ${APP_NAME2} stop`);
8359
+ console.log(` - Check status: ${APP_NAME3} status`);
8360
+ console.log(` - If you need to stop the service, run: ${APP_NAME3} stop`);
7770
8361
  }
7771
8362
  async startUiIfEnabled(uiConfig, uiStaticDir, cronService, runtimePool, sessionManager, providerManager, bus, gatewayController, getConfig, getExtensionRegistry, resolveMessageToolHints) {
7772
8363
  if (!uiConfig.enabled) {
@@ -7855,7 +8446,7 @@ var ServiceCommands = class {
7855
8446
  const uiServer = startUiServer({
7856
8447
  host: uiConfig.host,
7857
8448
  port: uiConfig.port,
7858
- configPath: getConfigPath5(),
8449
+ configPath: getConfigPath6(),
7859
8450
  productVersion: getPackageVersion(),
7860
8451
  staticDir: uiStaticDir ?? void 0,
7861
8452
  cronService,
@@ -7943,7 +8534,7 @@ var ServiceCommands = class {
7943
8534
  }
7944
8535
  }
7945
8536
  installBuiltinMarketplaceSkill(slug, force) {
7946
- const workspace = getWorkspacePath9(loadConfig12().agents.defaults.workspace);
8537
+ const workspace = getWorkspacePath9(loadConfig14().agents.defaults.workspace);
7947
8538
  const destination = join7(workspace, "skills", slug);
7948
8539
  const destinationSkillFile = join7(destination, "SKILL.md");
7949
8540
  if (existsSync11(destinationSkillFile) && !force) {
@@ -8020,12 +8611,58 @@ ${stderr}`.trim();
8020
8611
  }
8021
8612
  };
8022
8613
 
8614
+ // src/cli/remote/remote-runtime-actions.ts
8615
+ import { APP_NAME as APP_NAME4 } from "@nextclaw/core";
8616
+ var RemoteRuntimeActions = class {
8617
+ constructor(deps) {
8618
+ this.deps = deps;
8619
+ }
8620
+ async connect(opts = {}) {
8621
+ await this.deps.remoteCommands.connect(opts);
8622
+ }
8623
+ async enable(opts = {}) {
8624
+ await this.deps.initAuto("remote enable");
8625
+ const result = this.deps.remoteCommands.enableConfig(opts);
8626
+ console.log("\u2713 Remote access enabled");
8627
+ if (result.config.remote.deviceName.trim()) {
8628
+ console.log(`Device: ${result.config.remote.deviceName.trim()}`);
8629
+ }
8630
+ if (result.config.remote.platformApiBase.trim()) {
8631
+ console.log(`Platform: ${result.config.remote.platformApiBase.trim()}`);
8632
+ }
8633
+ if (this.hasRunningManagedService()) {
8634
+ await this.deps.restartBackgroundService("remote configuration updated");
8635
+ console.log("\u2713 Applied remote settings to running background service");
8636
+ return;
8637
+ }
8638
+ console.log(`Tip: Run "${APP_NAME4} start" to bring the managed remote connector online.`);
8639
+ }
8640
+ async disable() {
8641
+ const result = this.deps.remoteCommands.disableConfig();
8642
+ console.log(result.changed ? "\u2713 Remote access disabled" : "Remote access was already disabled");
8643
+ if (this.hasRunningManagedService()) {
8644
+ await this.deps.restartBackgroundService("remote access disabled");
8645
+ console.log("\u2713 Running background service restarted without remote access");
8646
+ }
8647
+ }
8648
+ async status(opts = {}) {
8649
+ await this.deps.remoteCommands.status(opts);
8650
+ }
8651
+ async doctor(opts = {}) {
8652
+ await this.deps.remoteCommands.doctor(opts);
8653
+ }
8654
+ hasRunningManagedService() {
8655
+ const state = readServiceState();
8656
+ return Boolean(state && isProcessRunning(state.pid));
8657
+ }
8658
+ };
8659
+
8023
8660
  // src/cli/workspace.ts
8024
8661
  import { cpSync as cpSync3, existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync10, readdirSync as readdirSync3, rmSync as rmSync5, writeFileSync as writeFileSync6 } from "fs";
8025
8662
  import { createRequire as createRequire2 } from "module";
8026
8663
  import { dirname as dirname4, join as join8, resolve as resolve11 } from "path";
8027
- import { fileURLToPath as fileURLToPath3 } from "url";
8028
- import { APP_NAME as APP_NAME3, getDataDir as getDataDir8 } from "@nextclaw/core";
8664
+ import { fileURLToPath as fileURLToPath4 } from "url";
8665
+ import { APP_NAME as APP_NAME5, getDataDir as getDataDir8 } from "@nextclaw/core";
8029
8666
  import { spawnSync as spawnSync3 } from "child_process";
8030
8667
  var WorkspaceManager = class {
8031
8668
  constructor(logo) {
@@ -8063,7 +8700,7 @@ var WorkspaceManager = class {
8063
8700
  continue;
8064
8701
  }
8065
8702
  const raw = readFileSync10(templatePath, "utf-8");
8066
- const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME3);
8703
+ const content = raw.replace(/\$\{APP_NAME\}/g, APP_NAME5);
8067
8704
  mkdirSync7(dirname4(filePath), { recursive: true });
8068
8705
  writeFileSync6(filePath, content);
8069
8706
  created.push(entry.target);
@@ -8136,7 +8773,7 @@ var WorkspaceManager = class {
8136
8773
  if (override) {
8137
8774
  return override;
8138
8775
  }
8139
- const cliDir = resolve11(fileURLToPath3(new URL(".", import.meta.url)));
8776
+ const cliDir = resolve11(fileURLToPath4(new URL(".", import.meta.url)));
8140
8777
  const pkgRoot = resolve11(cliDir, "..", "..");
8141
8778
  const candidates = [join8(pkgRoot, "templates")];
8142
8779
  for (const candidate of candidates) {
@@ -8155,7 +8792,7 @@ var WorkspaceManager = class {
8155
8792
  console.error("npm not found. Please install Node.js >= 18.");
8156
8793
  process.exit(1);
8157
8794
  }
8158
- const cliDir = resolve11(fileURLToPath3(new URL(".", import.meta.url)));
8795
+ const cliDir = resolve11(fileURLToPath4(new URL(".", import.meta.url)));
8159
8796
  const pkgRoot = resolve11(cliDir, "..", "..");
8160
8797
  const pkgBridge = join8(pkgRoot, "bridge");
8161
8798
  const srcBridge = join8(pkgRoot, "..", "..", "bridge");
@@ -8166,7 +8803,7 @@ var WorkspaceManager = class {
8166
8803
  source = srcBridge;
8167
8804
  }
8168
8805
  if (!source) {
8169
- console.error(`Bridge source not found. Try reinstalling ${APP_NAME3}.`);
8806
+ console.error(`Bridge source not found. Try reinstalling ${APP_NAME5}.`);
8170
8807
  process.exit(1);
8171
8808
  }
8172
8809
  console.log(`${this.logo} Setting up bridge...`);
@@ -8224,6 +8861,7 @@ var CliRuntime = class {
8224
8861
  cronCommands;
8225
8862
  platformAuthCommands;
8226
8863
  remoteCommands;
8864
+ remote;
8227
8865
  diagnosticsCommands;
8228
8866
  constructor(options = {}) {
8229
8867
  this.logo = options.logo ?? LOGO;
@@ -8247,6 +8885,11 @@ var CliRuntime = class {
8247
8885
  this.cronCommands = new CronCommands();
8248
8886
  this.platformAuthCommands = new PlatformAuthCommands();
8249
8887
  this.remoteCommands = new RemoteCommands();
8888
+ this.remote = new RemoteRuntimeActions({
8889
+ initAuto: (source) => this.init({ source, auto: true }),
8890
+ remoteCommands: this.remoteCommands,
8891
+ restartBackgroundService: (reason) => this.restartBackgroundService(reason)
8892
+ });
8250
8893
  this.diagnosticsCommands = new DiagnosticsCommands({ logo: this.logo });
8251
8894
  this.restartCoordinator = new RestartCoordinator({
8252
8895
  readServiceState,
@@ -8277,7 +8920,7 @@ var CliRuntime = class {
8277
8920
  const uiHost = FORCED_PUBLIC_UI_HOST;
8278
8921
  const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
8279
8922
  console.log(
8280
- `Applying changes (${reason}): restarting ${APP_NAME4} background service...`
8923
+ `Applying changes (${reason}): restarting ${APP_NAME6} background service...`
8281
8924
  );
8282
8925
  await this.serviceCommands.stopService();
8283
8926
  await this.serviceCommands.startService({
@@ -8310,7 +8953,7 @@ var CliRuntime = class {
8310
8953
  }
8311
8954
  const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 18791;
8312
8955
  const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
8313
- const cliPath = process.env.NEXTCLAW_SELF_RELAUNCH_CLI?.trim() || fileURLToPath4(new URL("./index.js", import.meta.url));
8956
+ const cliPath = process.env.NEXTCLAW_SELF_RELAUNCH_CLI?.trim() || fileURLToPath5(new URL("./index.js", import.meta.url));
8314
8957
  const startArgs = [cliPath, "start", "--ui-port", String(uiPort)];
8315
8958
  const serviceStatePath = resolve12(getDataDir9(), "run", "service.json");
8316
8959
  const helperScript = [
@@ -8432,7 +9075,7 @@ var CliRuntime = class {
8432
9075
  }
8433
9076
  async onboard() {
8434
9077
  console.warn(
8435
- `Warning: ${APP_NAME4} onboard is deprecated. Use "${APP_NAME4} init" instead.`
9078
+ `Warning: ${APP_NAME6} onboard is deprecated. Use "${APP_NAME6} init" instead.`
8436
9079
  );
8437
9080
  await this.init({ source: "onboard" });
8438
9081
  }
@@ -8440,14 +9083,14 @@ var CliRuntime = class {
8440
9083
  const source = options.source ?? "init";
8441
9084
  const prefix = options.auto ? "Auto init" : "Init";
8442
9085
  const force = Boolean(options.force);
8443
- const configPath = getConfigPath6();
9086
+ const configPath = getConfigPath7();
8444
9087
  let createdConfig = false;
8445
9088
  if (!existsSync13(configPath)) {
8446
9089
  const config3 = ConfigSchema2.parse({});
8447
- saveConfig9(config3);
9090
+ saveConfig10(config3);
8448
9091
  createdConfig = true;
8449
9092
  }
8450
- const config2 = loadConfig13();
9093
+ const config2 = loadConfig15();
8451
9094
  const workspaceSetting = config2.agents.defaults.workspace;
8452
9095
  const workspacePath = !workspaceSetting || workspaceSetting === DEFAULT_WORKSPACE_PATH ? join9(getDataDir9(), DEFAULT_WORKSPACE_DIR) : expandHome2(workspaceSetting);
8453
9096
  const workspaceExisted = existsSync13(workspacePath);
@@ -8470,13 +9113,13 @@ var CliRuntime = class {
8470
9113
  }
8471
9114
  if (!options.auto) {
8472
9115
  console.log(`
8473
- ${this.logo} ${APP_NAME4} is ready! (${source})`);
9116
+ ${this.logo} ${APP_NAME6} is ready! (${source})`);
8474
9117
  console.log("\nNext steps:");
8475
9118
  console.log(` 1. Add your API key to ${configPath}`);
8476
- console.log(` 2. Chat: ${APP_NAME4} agent -m "Hello!"`);
9119
+ console.log(` 2. Chat: ${APP_NAME6} agent -m "Hello!"`);
8477
9120
  } else {
8478
9121
  console.log(
8479
- `Tip: Run "${APP_NAME4} init${force ? " --force" : ""}" to re-run initialization if needed.`
9122
+ `Tip: Run "${APP_NAME6} init${force ? " --force" : ""}" to re-run initialization if needed.`
8480
9123
  );
8481
9124
  }
8482
9125
  }
@@ -8484,9 +9127,6 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8484
9127
  await this.init({ source: "login", auto: true });
8485
9128
  await this.platformAuthCommands.login(opts);
8486
9129
  }
8487
- async remoteConnect(opts = {}) {
8488
- await this.remoteCommands.connect(opts);
8489
- }
8490
9130
  async gateway(opts) {
8491
9131
  const uiOverrides = {
8492
9132
  host: FORCED_PUBLIC_UI_HOST
@@ -8537,7 +9177,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8537
9177
  await this.writeRestartSentinelFromExecContext("cli.restart");
8538
9178
  const state = readServiceState();
8539
9179
  if (state && isProcessRunning(state.pid)) {
8540
- console.log(`Restarting ${APP_NAME4}...`);
9180
+ console.log(`Restarting ${APP_NAME6}...`);
8541
9181
  await this.serviceCommands.stopService();
8542
9182
  } else if (state) {
8543
9183
  clearServiceState();
@@ -8576,8 +9216,8 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8576
9216
  await this.serviceCommands.stopService();
8577
9217
  }
8578
9218
  async agent(opts) {
8579
- const configPath = getConfigPath6();
8580
- const config2 = resolveConfigSecrets3(loadConfig13(), { configPath });
9219
+ const configPath = getConfigPath7();
9220
+ const config2 = resolveConfigSecrets3(loadConfig15(), { configPath });
8581
9221
  const workspace = getWorkspacePath10(config2.agents.defaults.workspace);
8582
9222
  const pluginRegistry = loadPluginRegistry(config2, workspace);
8583
9223
  const extensionRegistry = toExtensionRegistry(pluginRegistry);
@@ -8585,7 +9225,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8585
9225
  const pluginChannelBindings = getPluginChannelBindings4(pluginRegistry);
8586
9226
  setPluginRuntimeBridge2({
8587
9227
  loadConfig: () => toPluginConfigView(
8588
- resolveConfigSecrets3(loadConfig13(), { configPath }),
9228
+ resolveConfigSecrets3(loadConfig15(), { configPath }),
8589
9229
  pluginChannelBindings
8590
9230
  ),
8591
9231
  writeConfigFile: async (nextConfigView) => {
@@ -8594,13 +9234,13 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8594
9234
  "plugin runtime writeConfigFile expects an object config"
8595
9235
  );
8596
9236
  }
8597
- const current = loadConfig13();
9237
+ const current = loadConfig15();
8598
9238
  const next = mergePluginConfigView(
8599
9239
  current,
8600
9240
  nextConfigView,
8601
9241
  pluginChannelBindings
8602
9242
  );
8603
- saveConfig9(next);
9243
+ saveConfig10(next);
8604
9244
  }
8605
9245
  });
8606
9246
  try {
@@ -8626,7 +9266,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8626
9266
  resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints2({
8627
9267
  registry: pluginRegistry,
8628
9268
  channel,
8629
- cfg: resolveConfigSecrets3(loadConfig13(), { configPath }),
9269
+ cfg: resolveConfigSecrets3(loadConfig15(), { configPath }),
8630
9270
  accountId
8631
9271
  })
8632
9272
  });
@@ -8727,7 +9367,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8727
9367
  }
8728
9368
  const state = readServiceState();
8729
9369
  if (state && isProcessRunning(state.pid)) {
8730
- console.log(`Tip: restart ${APP_NAME4} to apply the update.`);
9370
+ console.log(`Tip: restart ${APP_NAME6} to apply the update.`);
8731
9371
  }
8732
9372
  }
8733
9373
  pluginsList(opts = {}) {
@@ -8821,7 +9461,7 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8821
9461
  await this.diagnosticsCommands.doctor(opts);
8822
9462
  }
8823
9463
  async skillsInstall(options) {
8824
- const config2 = loadConfig13();
9464
+ const config2 = loadConfig15();
8825
9465
  const workdir = resolveSkillsInstallWorkdir({
8826
9466
  explicitWorkdir: options.workdir,
8827
9467
  configuredWorkspace: config2.agents.defaults.workspace
@@ -8882,23 +9522,32 @@ ${this.logo} ${APP_NAME4} is ready! (${source})`);
8882
9522
  }
8883
9523
  };
8884
9524
 
9525
+ // src/cli/remote/register-remote-commands.ts
9526
+ function registerRemoteCommands(program2, runtime2) {
9527
+ const remote = program2.command("remote").description("Manage remote access");
9528
+ remote.command("enable").description("Enable service-managed remote access").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--name <name>", "Device display name").action(async (opts) => runtime2.enable(opts));
9529
+ remote.command("disable").description("Disable service-managed remote access").action(async () => runtime2.disable());
9530
+ remote.command("status").description("Show remote access status").option("--json", "Print JSON").action(async (opts) => runtime2.status(opts));
9531
+ remote.command("doctor").description("Run remote access diagnostics").option("--json", "Print JSON").action(async (opts) => runtime2.doctor(opts));
9532
+ remote.command("connect").description("Foreground debug mode: register this machine and keep the connector online").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--local-origin <url>", "Local NextClaw UI origin (default: active service or http://127.0.0.1:18791)").option("--name <name>", "Device display name").option("--once", "Connect once without auto-reconnect", false).action(async (opts) => runtime2.connect(opts));
9533
+ }
9534
+
8885
9535
  // src/cli/index.ts
8886
9536
  var program = new Command();
8887
9537
  var runtime = new CliRuntime({ logo: LOGO });
8888
- program.name(APP_NAME5).description(`${LOGO} ${APP_NAME5} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
8889
- program.command("onboard").description(`Initialize ${APP_NAME5} configuration and workspace`).action(async () => runtime.onboard());
8890
- program.command("init").description(`Initialize ${APP_NAME5} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
9538
+ program.name(APP_NAME7).description(`${LOGO} ${APP_NAME7} - ${APP_TAGLINE}`).version(getPackageVersion(), "-v, --version", "show version");
9539
+ program.command("onboard").description(`Initialize ${APP_NAME7} configuration and workspace`).action(async () => runtime.onboard());
9540
+ program.command("init").description(`Initialize ${APP_NAME7} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
8891
9541
  program.command("login").description("Login to NextClaw platform and save token into providers.nextclaw.apiKey").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--email <email>", "Login email").option("--password <password>", "Login password").option("--register", "Register first, then login", false).action(async (opts) => runtime.login(opts));
8892
- var remote = program.command("remote").description("Manage remote access");
8893
- remote.command("connect").description("Register this machine as a remote device and keep the connector online").option("--api-base <url>", "Platform API base (supports /v1 suffix)").option("--local-origin <url>", "Local NextClaw UI origin (default: active service or http://127.0.0.1:18791)").option("--name <name>", "Device display name").option("--once", "Connect once without auto-reconnect", false).action(async (opts) => runtime.remoteConnect(opts));
8894
- program.command("gateway").description(`Start the ${APP_NAME5} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
8895
- program.command("ui").description(`Start the ${APP_NAME5} UI with gateway`).option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
8896
- program.command("start").description(`Start the ${APP_NAME5} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
8897
- program.command("restart").description(`Restart the ${APP_NAME5} background service`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
8898
- program.command("serve").description(`Run the ${APP_NAME5} gateway + UI in the foreground`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
8899
- program.command("stop").description(`Stop the ${APP_NAME5} background service`).action(async () => runtime.stop());
9542
+ registerRemoteCommands(program, runtime.remote);
9543
+ program.command("gateway").description(`Start the ${APP_NAME7} gateway`).option("-p, --port <port>", "Gateway port", "18790").option("-v, --verbose", "Verbose output", false).option("--ui", "Enable UI server", false).option("--ui-port <port>", "UI port").option("--ui-open", "Open browser when UI starts", false).action(async (opts) => runtime.gateway(opts));
9544
+ program.command("ui").description(`Start the ${APP_NAME7} UI with gateway`).option("--port <port>", "UI port").option("--no-open", "Disable opening browser").action(async (opts) => runtime.ui(opts));
9545
+ program.command("start").description(`Start the ${APP_NAME7} gateway + UI in the background`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after start", false).action(async (opts) => runtime.start(opts));
9546
+ program.command("restart").description(`Restart the ${APP_NAME7} background service`).option("--ui-port <port>", "UI port").option("--start-timeout <ms>", "Maximum wait time for startup readiness in milliseconds").option("--open", "Open browser after restart", false).action(async (opts) => runtime.restart(opts));
9547
+ program.command("serve").description(`Run the ${APP_NAME7} gateway + UI in the foreground`).option("--ui-port <port>", "UI port").option("--open", "Open browser after start", false).action(async (opts) => runtime.serve(opts));
9548
+ program.command("stop").description(`Stop the ${APP_NAME7} background service`).action(async () => runtime.stop());
8900
9549
  program.command("agent").description("Interact with the agent directly").option("-m, --message <message>", "Message to send to the agent").option("-s, --session <session>", "Session ID", "cli:default").option("--model <model>", "Session model override for this run").option("--no-markdown", "Disable Markdown rendering").action(async (opts) => runtime.agent(opts));
8901
- program.command("update").description(`Update ${APP_NAME5}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));
9550
+ program.command("update").description(`Update ${APP_NAME7}`).option("--timeout <ms>", "Update command timeout in milliseconds").action(async (opts) => runtime.update(opts));
8902
9551
  var skills = program.command("skills").description("Manage skills");
8903
9552
  skills.command("install <slug>").description("Install a skill from NextClaw marketplace").option("--api-base <url>", "Marketplace API base URL").option("--workdir <dir>", "Workspace directory to install into").option("--dir <dir>", "Skills directory name (default: skills)").option("-f, --force", "Overwrite existing skill files", false).action(async (slug, opts) => runtime.skillsInstall({ slug, ...opts, apiBaseUrl: opts.apiBase }));
8904
9553
  var withRepeatableTag = (value, previous = []) => [...previous, value];
@@ -8943,6 +9592,6 @@ cron.command("add").requiredOption("-n, --name <name>", "Job name").requiredOpti
8943
9592
  cron.command("remove <jobId>").action((jobId) => runtime.cronRemove(jobId));
8944
9593
  cron.command("enable <jobId>").option("--disable", "Disable instead of enable").action((jobId, opts) => runtime.cronEnable(jobId, opts));
8945
9594
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
8946
- program.command("status").description(`Show ${APP_NAME5} status`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.status(opts));
8947
- program.command("doctor").description(`Run ${APP_NAME5} diagnostics`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.doctor(opts));
9595
+ program.command("status").description(`Show ${APP_NAME7} status`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.status(opts));
9596
+ program.command("doctor").description(`Run ${APP_NAME7} diagnostics`).option("--json", "Output JSON", false).option("--verbose", "Show extra diagnostics", false).option("--fix", "Fix stale service state when safe", false).action(async (opts) => runtime.doctor(opts));
8948
9597
  program.parseAsync(process.argv);