nextclaw 0.17.6 → 0.17.9

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.
Files changed (84) hide show
  1. package/dist/cli/index.js +1926 -979
  2. package/package.json +12 -12
  3. package/resources/USAGE.md +21 -1
  4. package/ui-dist/assets/ChannelsList-D8p4OlM6.js +8 -0
  5. package/ui-dist/assets/ChatPage-A45t1Rmf.js +58 -0
  6. package/ui-dist/assets/DocBrowser-B2MpsnU9.js +1 -0
  7. package/ui-dist/assets/{DocBrowser-QUZ3nfmH.js → DocBrowser-Cse_F8Nn.js} +1 -1
  8. package/ui-dist/assets/{DocBrowserContext-CpiIfhJO.js → DocBrowserContext-Bai1WU2H.js} +1 -1
  9. package/ui-dist/assets/{LogoBadge-BUK13xK5.js → LogoBadge-BdxMPc9v.js} +1 -1
  10. package/ui-dist/assets/MarketplacePage-BNZ3Jx5d.js +1 -0
  11. package/ui-dist/assets/MarketplacePage-BbpAkllU.js +49 -0
  12. package/ui-dist/assets/{McpMarketplacePage-BG4T_Pcx.js → McpMarketplacePage-CxPFOgxv.js} +2 -2
  13. package/ui-dist/assets/ModelConfig-3GLqQ5GY.js +1 -0
  14. package/ui-dist/assets/{ProviderScopedModelInput-DGn6sFEN.js → ProviderScopedModelInput-BYNouw-i.js} +1 -1
  15. package/ui-dist/assets/ProvidersList-BR1gJ4Dm.js +1 -0
  16. package/ui-dist/assets/{RemoteAccessPage-ff15qO-c.js → RemoteAccessPage-DyYVWsyK.js} +1 -1
  17. package/ui-dist/assets/{RuntimeConfig-TgPandXF.js → RuntimeConfig-ChdfK4Y_.js} +1 -1
  18. package/ui-dist/assets/SearchConfig-DTeJvp8m.js +1 -0
  19. package/ui-dist/assets/{SecretsConfig-Bew4EF2A.js → SecretsConfig-CCYO6NcV.js} +2 -2
  20. package/ui-dist/assets/SessionsConfig-Du39vDgt.js +2 -0
  21. package/ui-dist/assets/app-query-client-Dr5d-K8d.js +1 -0
  22. package/ui-dist/assets/{book-open-CJG8Yz3U.js → book-open-Da4OEPqB.js} +1 -1
  23. package/ui-dist/assets/chat-session-display-CAlPrnlV.js +1 -0
  24. package/ui-dist/assets/{chunk-JZWAC4HX-D5b3Iyas.js → chunk-JZWAC4HX-CoFVxHXV.js} +1 -1
  25. package/ui-dist/assets/client-CSk58DcF.js +7 -0
  26. package/ui-dist/assets/config-D8KzikVB.js +1 -0
  27. package/ui-dist/assets/{createLucideIcon-_FMJqZw2.js → createLucideIcon-83gaZMtv.js} +1 -1
  28. package/ui-dist/assets/desktop-update-config-CfoVwf-w.js +1 -0
  29. package/ui-dist/assets/dist-aTmhMDVh.js +9 -0
  30. package/ui-dist/assets/{dist-B1fpOuON.js → dist-toEYs-MZ.js} +1 -1
  31. package/ui-dist/assets/{external-link-b7gAJWYY.js → external-link-QQ0TC6X4.js} +1 -1
  32. package/ui-dist/assets/{hash-Bhy4TwfZ.js → hash-DaFBEkmi.js} +1 -1
  33. package/ui-dist/assets/i18n-C3jb83S6.js +1 -0
  34. package/ui-dist/assets/index-CE4N7ItL.css +1 -0
  35. package/ui-dist/assets/index-riX7Sg0_.js +6 -0
  36. package/ui-dist/assets/infiniteQueryBehavior-BmHX_ayZ.js +1 -0
  37. package/ui-dist/assets/loader-circle-BjMg63eu.js +1 -0
  38. package/ui-dist/assets/{logos-GMeYU9vc.js → logos-Dzlz30M3.js} +1 -1
  39. package/ui-dist/assets/{page-layout-C8UbWuMt.js → page-layout-D2eRufRQ.js} +1 -1
  40. package/ui-dist/assets/plus-CIXME2pD.js +1 -0
  41. package/ui-dist/assets/{popover-8HSx9wQj.js → popover-BSXxm5bj.js} +1 -1
  42. package/ui-dist/assets/{refresh-ccw-CA4_C7Zg.js → refresh-ccw-B3zMtN-_.js} +1 -1
  43. package/ui-dist/assets/refresh-cw-DlZkIHnJ.js +1 -0
  44. package/ui-dist/assets/{save-BtvMy4lk.js → save-Us9fg4Sj.js} +1 -1
  45. package/ui-dist/assets/search-B_Qr0f6C.js +1 -0
  46. package/ui-dist/assets/security-config-BGWYwxNr.js +1 -0
  47. package/ui-dist/assets/{select-xp_Ac8ip.js → select-DLYqySQK.js} +1 -1
  48. package/ui-dist/assets/skeleton-CYQJazv6.js +1 -0
  49. package/ui-dist/assets/{status-dot-Cn4Pp7DZ.js → status-dot-DGayudyB.js} +1 -1
  50. package/ui-dist/assets/{switch-BTi6UOij.js → switch-Dz2ScsKx.js} +1 -1
  51. package/ui-dist/assets/{tabs-custom-BiiN8DME.js → tabs-custom-CdKyjiGk.js} +1 -1
  52. package/ui-dist/assets/{trash-2-BpsF0N-r.js → trash-2-Db-mZOZs.js} +1 -1
  53. package/ui-dist/assets/use-infinite-scroll-loader-DBJX5hj0.js +1 -0
  54. package/ui-dist/assets/{useConfirmDialog-BJIwUZjH.js → useConfirmDialog-DL0a-oGC.js} +1 -1
  55. package/ui-dist/assets/useMutation-BdZm-9PL.js +1 -0
  56. package/ui-dist/assets/x-B8Tho_xC.js +1 -0
  57. package/ui-dist/index.html +20 -19
  58. package/ui-dist/assets/ChannelsList-C6-lh55g.js +0 -8
  59. package/ui-dist/assets/ChatPage-DOW0gPc2.js +0 -45
  60. package/ui-dist/assets/DocBrowser-CGyeswYP.js +0 -1
  61. package/ui-dist/assets/MarketplacePage-BDVwhIYE.js +0 -1
  62. package/ui-dist/assets/MarketplacePage-LnKKL3xK.js +0 -49
  63. package/ui-dist/assets/ModelConfig-LtWuogIw.js +0 -1
  64. package/ui-dist/assets/ProvidersList-ma-_MlLo.js +0 -1
  65. package/ui-dist/assets/SearchConfig-C9iBt7pl.js +0 -1
  66. package/ui-dist/assets/SessionsConfig-2r2yAGZg.js +0 -2
  67. package/ui-dist/assets/chat-session-display-DkAC5OMC.js +0 -1
  68. package/ui-dist/assets/config-zvnxSXSP.js +0 -1
  69. package/ui-dist/assets/dist-BCXX7FD-.js +0 -15
  70. package/ui-dist/assets/i18n-DJg9BPYk.js +0 -1
  71. package/ui-dist/assets/index-BoJbxdvZ.css +0 -1
  72. package/ui-dist/assets/index-CtlT4E9Y.js +0 -6
  73. package/ui-dist/assets/infiniteQueryBehavior-CTcVlD9s.js +0 -1
  74. package/ui-dist/assets/loader-circle-B60I0hEk.js +0 -1
  75. package/ui-dist/assets/plus-CR7RfK3H.js +0 -1
  76. package/ui-dist/assets/react-BB4jko2M.js +0 -1
  77. package/ui-dist/assets/search-C60UA27E.js +0 -1
  78. package/ui-dist/assets/security-config-BkFDYZ6j.js +0 -1
  79. package/ui-dist/assets/skeleton-uxz_5h3A.js +0 -1
  80. package/ui-dist/assets/use-infinite-scroll-loader-C8jBv11-.js +0 -1
  81. package/ui-dist/assets/useMutation-BjBOKHj_.js +0 -1
  82. package/ui-dist/assets/x-BfTu-g7D.js +0 -1
  83. /package/ui-dist/assets/{config-hints-WtpHP_DW.js → config-hints-GSUMvmSo.js} +0 -0
  84. /package/ui-dist/assets/{config-layout-LQ10ozRC.js → config-layout-CgBMG7OL.js} +0 -0
package/dist/cli/index.js CHANGED
@@ -1,17 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import * as NextclawCore from "@nextclaw/core";
4
- import { APP_NAME, APP_TAGLINE, AgentLoop, AgentRouteResolver, BUILTIN_MAIN_AGENT_ID, ChannelManager, CommandRegistry, ConfigSchema, ContextBuilder, CronService, CronTool, DEFAULT_WORKSPACE_DIR, DEFAULT_WORKSPACE_PATH, DisposableStore, EditFileTool, ExecTool, ExtensionToolAdapter, GatewayTool, InputBudgetPruner, LLMProvider, ListDirTool, MemoryGetTool, MemorySearchTool, MessageBus, MessageTool, NativeAgentEngine, ProviderManager, ReadFileTool, SessionsHistoryTool, SessionsListTool, SkillsLoader, Tool, ToolRegistry, WebFetchTool, WebSearchTool, WriteFileTool, buildConfigSchema, buildReloadPlan, buildToolCatalogEntries, createAgentProfile, createAssistantStreamDeltaControlMessage, createAssistantStreamResetControlMessage, createExternalCommandEnv, diffConfigPaths, expandHome, findEffectiveAgentProfile, getConfigPath, getDataDir, getPackageVersion, getWorkspacePath, hasSecretRef, loadConfig, normalizeInlineSecretRefs, parseAgentScopedSessionKey, parseThinkingLevel, readSessionProjectRoot, redactConfigObject, removeAgentProfile, resolveConfigSecrets, resolveDefaultAgentProfileId, resolveEffectiveAgentProfiles, resolveSessionWorkspacePath, resolveThinkingLevel, saveConfig, toDisposable, updateAgentProfile } from "@nextclaw/core";
4
+ import { APP_NAME, APP_TAGLINE, AgentRouteResolver, BUILTIN_MAIN_AGENT_ID, ChannelManager, CommandRegistry, ConfigSchema, ContextBuilder, CronService, CronTool, DEFAULT_WORKSPACE_DIR, DEFAULT_WORKSPACE_PATH, DisposableStore, EditFileTool, ExecTool, ExtensionToolAdapter, FileLogSink, GatewayTool, InputBudgetPruner, LLMProvider, ListDirTool, MemoryGetTool, MemorySearchTool, MessageBus, MessageTool, ProviderManager, ReadFileTool, SessionManager, SessionsHistoryTool, SessionsListTool, SkillsLoader, Tool, ToolRegistry, WebFetchTool, WebSearchTool, WriteFileTool, buildConfigSchema, buildMinimalSystemExecutionPrompt, buildReloadPlan, buildToolCatalogEntries, createAgentProfile, createAssistantStreamDeltaControlMessage, createAssistantStreamResetControlMessage, createExternalCommandEnv, createTypingStopControlMessage, diffConfigPaths, expandHome, findEffectiveAgentProfile, getAppLogger, getConfigPath, getDataDir, getLoggingRuntime, getLogsPath, getPackageVersion, getWorkspacePath, hasSecretRef, loadConfig, normalizeInlineSecretRefs, parseAgentScopedSessionKey, parseThinkingLevel, readSessionProjectRoot, redactConfigObject, removeAgentProfile, resolveAppLogPath, resolveConfigSecrets, resolveDefaultAgentProfileId, resolveEffectiveAgentProfiles, resolveLocalUiBaseUrl, resolveSessionWorkspacePath, resolveThinkingLevel, saveConfig, toDisposable, updateAgentProfile } from "@nextclaw/core";
5
5
  import { Command } from "commander";
6
6
  import fs, { appendFileSync, closeSync, cpSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
7
7
  import path, { basename, dirname, extname, isAbsolute, join, relative, resolve } from "node:path";
8
8
  import { hostname, platform } from "node:os";
9
9
  import { ensureUiBridgeSecret, startUiServer } from "@nextclaw/server";
10
- import { addPluginLoadPath, buildPluginStatusReport, disablePluginInConfig, discoverPluginStatusReport, enablePluginInConfig, getPluginChannelBindings, getPluginUiMetadataFromRegistry, installPluginFromNpmSpec, installPluginFromPath, loadOpenClawPlugins, loadOpenClawPluginsProgressively, mergePluginConfigView, recordPluginInstall, resolvePluginChannelMessageToolHints, resolveUninstallDirectoryTargets, setPluginRuntimeBridge, startPluginChannelGateways, stopPluginChannelGateways, toPluginConfigView, toPluginConfigView as toPluginConfigView$1, uninstallPlugin } from "@nextclaw/openclaw-compat";
11
- import { createInterface } from "node:readline";
10
+ import { addPluginLoadPath, buildPluginStatusReport, disablePluginInConfig, discoverPluginStatusReport, enablePluginInConfig, getPackageManifestExtensions, getPluginChannelBindings, getPluginUiMetadataFromRegistry, installPluginFromNpmSpec, installPluginFromPath, loadOpenClawPlugins, loadOpenClawPluginsProgressively, loadPluginManifest, mergePluginConfigView, recordPluginInstall, resolvePluginChannelMessageToolHints, resolveUninstallDirectoryTargets, setPluginRuntimeBridge, startPluginChannelGateways, stopPluginChannelGateways, toPluginConfigView, toPluginConfigView as toPluginConfigView$1, uninstallPlugin } from "@nextclaw/openclaw-compat";
12
11
  import { fileURLToPath } from "node:url";
13
12
  import { spawn, spawnSync } from "node:child_process";
14
13
  import { parse } from "yaml";
14
+ import { createInterface } from "node:readline";
15
15
  import { createServer, isIP } from "node:net";
16
16
  import { BUILTIN_CHANNEL_PLUGIN_IDS, builtinProviderIds, listBuiltinProviders } from "@nextclaw/runtime";
17
17
  import { McpDoctorFacade, McpMutationService, McpRegistryService, McpServerLifecycleManager } from "@nextclaw/mcp";
@@ -24,6 +24,7 @@ import { McpNcpToolRegistryAdapter } from "@nextclaw/ncp-mcp";
24
24
  import { NCP_INTERNAL_VISIBILITY_METADATA_KEY, NcpEventType, readAssistantReasoningNormalizationMode, readAssistantReasoningNormalizationModeFromMetadata, sanitizeAssistantReplyTags, writeAssistantReasoningNormalizationModeToMetadata } from "@nextclaw/ncp";
25
25
  import { DefaultNcpAgentBackend, createAgentClientFromServer } from "@nextclaw/ncp-toolkit";
26
26
  import { createHash, randomUUID } from "node:crypto";
27
+ import { readFile } from "node:fs/promises";
27
28
  //#region \0rolldown/runtime.js
28
29
  var __create = Object.create;
29
30
  var __defProp = Object.defineProperty;
@@ -161,7 +162,7 @@ function maskToken(value) {
161
162
  if (value.length <= 12) return "<redacted>";
162
163
  return `${value.slice(0, 6)}...${value.slice(-4)}`;
163
164
  }
164
- function normalizeOptionalString$7(value) {
165
+ function normalizeOptionalString$9(value) {
165
166
  if (typeof value !== "string") return;
166
167
  const trimmed = value.trim();
167
168
  return trimmed.length > 0 ? trimmed : void 0;
@@ -259,8 +260,8 @@ var RemotePlatformClient = class {
259
260
  if (tokenState.reason === "missing") throw new Error("NextClaw platform token is missing. Run \"nextclaw login\" first.");
260
261
  if (tokenState.reason === "expired") throw new Error("NextClaw platform token expired. Run \"nextclaw login\" or browser sign-in again.");
261
262
  if (tokenState.reason === "malformed") throw new Error("NextClaw platform token is invalid. Run \"nextclaw login\" again.");
262
- const configuredApiBase = normalizeOptionalString$7(config.remote.platformApiBase) ?? (typeof nextclawProvider?.apiBase === "string" ? nextclawProvider.apiBase.trim() : "");
263
- const rawApiBase = normalizeOptionalString$7(opts.apiBase) ?? configuredApiBase;
263
+ const configuredApiBase = normalizeOptionalString$9(config.remote.platformApiBase) ?? (typeof nextclawProvider?.apiBase === "string" ? nextclawProvider.apiBase.trim() : "");
264
+ const rawApiBase = normalizeOptionalString$9(opts.apiBase) ?? configuredApiBase;
264
265
  if (!rawApiBase) throw new Error("Platform API base is missing. Pass --api-base, run nextclaw login, or set remote.platformApiBase.");
265
266
  return {
266
267
  platformBase: this.deps.resolvePlatformBase(rawApiBase),
@@ -269,14 +270,14 @@ var RemotePlatformClient = class {
269
270
  };
270
271
  }
271
272
  resolveLocalOrigin(config, opts) {
272
- const explicitOrigin = normalizeOptionalString$7(opts.localOrigin);
273
+ const explicitOrigin = normalizeOptionalString$9(opts.localOrigin);
273
274
  if (explicitOrigin) return explicitOrigin.replace(/\/$/, "");
274
275
  const state = this.deps.readManagedServiceState?.();
275
276
  if (state && this.deps.isProcessRunning?.(state.pid) && Number.isFinite(state.uiPort)) return `http://127.0.0.1:${state.uiPort}`;
276
277
  return `http://127.0.0.1:${typeof config.ui?.port === "number" && Number.isFinite(config.ui.port) ? config.ui.port : 55667}`;
277
278
  }
278
279
  resolveDisplayName(config, opts) {
279
- return normalizeOptionalString$7(opts.name) ?? normalizeOptionalString$7(config.remote.deviceName) ?? hostname();
280
+ return normalizeOptionalString$9(opts.name) ?? normalizeOptionalString$9(config.remote.deviceName) ?? hostname();
280
281
  }
281
282
  };
282
283
  //#endregion
@@ -4486,7 +4487,7 @@ var RemoteConnector = class {
4486
4487
  };
4487
4488
  //#endregion
4488
4489
  //#region ../nextclaw-remote/src/remote-status-store.ts
4489
- function normalizeOptionalString$6(value) {
4490
+ function normalizeOptionalString$8(value) {
4490
4491
  if (typeof value !== "string") return;
4491
4492
  const trimmed = value.trim();
4492
4493
  return trimmed.length > 0 ? trimmed : void 0;
@@ -4497,8 +4498,8 @@ function buildConfiguredRemoteState(config) {
4497
4498
  enabled: Boolean(remote.enabled),
4498
4499
  mode: "service",
4499
4500
  state: remote.enabled ? "disconnected" : "disabled",
4500
- ...normalizeOptionalString$6(remote.deviceName) ? { deviceName: normalizeOptionalString$6(remote.deviceName) } : {},
4501
- ...normalizeOptionalString$6(remote.platformApiBase) ? { platformBase: normalizeOptionalString$6(remote.platformApiBase) } : {},
4501
+ ...normalizeOptionalString$8(remote.deviceName) ? { deviceName: normalizeOptionalString$8(remote.deviceName) } : {},
4502
+ ...normalizeOptionalString$8(remote.platformApiBase) ? { platformBase: normalizeOptionalString$8(remote.platformApiBase) } : {},
4502
4503
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
4503
4504
  };
4504
4505
  }
@@ -4511,7 +4512,7 @@ function resolveRemoteStatusSnapshot(params) {
4511
4512
  configuredEnabled: true,
4512
4513
  runtime: {
4513
4514
  ...buildConfiguredRemoteState(params.config),
4514
- deviceName: normalizeOptionalString$6(params.config.remote.deviceName) ?? normalizeOptionalString$6(params.fallbackDeviceName) ?? hostname()
4515
+ deviceName: normalizeOptionalString$8(params.config.remote.deviceName) ?? normalizeOptionalString$8(params.fallbackDeviceName) ?? hostname()
4515
4516
  }
4516
4517
  };
4517
4518
  return {
@@ -4623,6 +4624,299 @@ var RemoteServiceModule = class {
4623
4624
  }
4624
4625
  };
4625
4626
  //#endregion
4627
+ //#region src/cli/runtime-state/llm-usage-history.store.ts
4628
+ var LlmUsageHistoryStore = class {
4629
+ constructor(explicitPath) {
4630
+ this.explicitPath = explicitPath;
4631
+ }
4632
+ get path() {
4633
+ return this.explicitPath ?? resolve(getDataDir(), "logs", "llm-usage.jsonl");
4634
+ }
4635
+ list = () => {
4636
+ if (!existsSync(this.path)) return [];
4637
+ try {
4638
+ return readFileSync(this.path, "utf-8").split("\n").map((line) => line.trim()).filter((line) => line.length > 0).flatMap((line) => {
4639
+ try {
4640
+ return [JSON.parse(line)];
4641
+ } catch {
4642
+ return [];
4643
+ }
4644
+ });
4645
+ } catch {
4646
+ return [];
4647
+ }
4648
+ };
4649
+ append = (record) => {
4650
+ mkdirSync(resolve(this.path, ".."), { recursive: true });
4651
+ appendFileSync(this.path, `${JSON.stringify(record)}\n`);
4652
+ };
4653
+ clear = () => {
4654
+ if (existsSync(this.path)) rmSync(this.path, { force: true });
4655
+ };
4656
+ };
4657
+ const llmUsageHistoryStore = new LlmUsageHistoryStore();
4658
+ //#endregion
4659
+ //#region src/cli/runtime-state/llm-usage-snapshot.store.ts
4660
+ var LlmUsageSnapshotStore = class {
4661
+ constructor(explicitPath) {
4662
+ this.explicitPath = explicitPath;
4663
+ }
4664
+ get path() {
4665
+ return this.explicitPath ?? resolve(getDataDir(), "run", "llm-usage.json");
4666
+ }
4667
+ read = () => {
4668
+ if (!existsSync(this.path)) return null;
4669
+ try {
4670
+ const raw = readFileSync(this.path, "utf-8");
4671
+ return JSON.parse(raw);
4672
+ } catch {
4673
+ return null;
4674
+ }
4675
+ };
4676
+ write = (snapshot) => {
4677
+ mkdirSync(resolve(this.path, ".."), { recursive: true });
4678
+ writeFileSync(this.path, JSON.stringify(snapshot, null, 2));
4679
+ };
4680
+ clear = () => {
4681
+ if (existsSync(this.path)) rmSync(this.path, { force: true });
4682
+ };
4683
+ };
4684
+ const llmUsageSnapshotStore = new LlmUsageSnapshotStore();
4685
+ //#endregion
4686
+ //#region src/cli/commands/shared/llm-usage-query.service.ts
4687
+ var LlmUsageQueryService = class {
4688
+ constructor(deps = {}) {
4689
+ this.deps = deps;
4690
+ }
4691
+ get snapshotPath() {
4692
+ return this.snapshotStore.path;
4693
+ }
4694
+ get historyPath() {
4695
+ return this.historyStore.path;
4696
+ }
4697
+ getSnapshot = () => {
4698
+ return this.snapshotStore.read();
4699
+ };
4700
+ getHistory = (limit) => {
4701
+ const records = this.historyStore.list();
4702
+ const resolvedLimit = this.resolveLimit(limit);
4703
+ return records.slice(-resolvedLimit).reverse();
4704
+ };
4705
+ getStats = () => {
4706
+ const records = this.historyStore.list();
4707
+ const sources = /* @__PURE__ */ new Map();
4708
+ const models = /* @__PURE__ */ new Map();
4709
+ let totalPromptTokens = 0;
4710
+ let totalCompletionTokens = 0;
4711
+ let totalTokens = 0;
4712
+ let totalCachedTokens = 0;
4713
+ let cacheHitRecords = 0;
4714
+ for (const record of records) {
4715
+ totalPromptTokens += record.summary.promptTokens;
4716
+ totalCompletionTokens += record.summary.completionTokens;
4717
+ totalTokens += record.summary.totalTokens;
4718
+ totalCachedTokens += record.summary.cachedTokens;
4719
+ if (record.summary.cacheHit) cacheHitRecords += 1;
4720
+ this.bumpCounter(sources, record.source);
4721
+ this.bumpCounter(models, record.model ?? "unknown");
4722
+ }
4723
+ return {
4724
+ totalRecords: records.length,
4725
+ oldestObservedAt: records[0]?.observedAt ?? null,
4726
+ latestObservedAt: records.at(-1)?.observedAt ?? null,
4727
+ totalPromptTokens,
4728
+ totalCompletionTokens,
4729
+ totalTokens,
4730
+ totalCachedTokens,
4731
+ cacheHitRecords,
4732
+ cacheHitRate: records.length > 0 ? cacheHitRecords / records.length : 0,
4733
+ sources: this.toSortedCounts(sources),
4734
+ models: this.toSortedCounts(models)
4735
+ };
4736
+ };
4737
+ get snapshotStore() {
4738
+ return this.deps.snapshotStore ?? llmUsageSnapshotStore;
4739
+ }
4740
+ get historyStore() {
4741
+ return this.deps.historyStore ?? llmUsageHistoryStore;
4742
+ }
4743
+ resolveLimit = (value) => {
4744
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.floor(value);
4745
+ if (typeof value === "string" && value.trim().length > 0) {
4746
+ const parsed = Number(value);
4747
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
4748
+ }
4749
+ return 10;
4750
+ };
4751
+ bumpCounter = (map, value) => {
4752
+ map.set(value, (map.get(value) ?? 0) + 1);
4753
+ };
4754
+ toSortedCounts = (map) => {
4755
+ return [...map.entries()].map(([value, count]) => ({
4756
+ value,
4757
+ count
4758
+ })).sort((left, right) => {
4759
+ if (right.count !== left.count) return right.count - left.count;
4760
+ return left.value.localeCompare(right.value);
4761
+ });
4762
+ };
4763
+ };
4764
+ const llmUsageQueryService = new LlmUsageQueryService();
4765
+ //#endregion
4766
+ //#region src/cli/commands/shared/llm-usage.commands.ts
4767
+ var LlmUsageCommands = class {
4768
+ constructor(deps = {}) {
4769
+ this.deps = deps;
4770
+ }
4771
+ show = async (opts = {}) => {
4772
+ if (opts.history && opts.stats) {
4773
+ console.error("Choose only one usage mode: `--history` or `--stats`.");
4774
+ process.exitCode = 1;
4775
+ return;
4776
+ }
4777
+ if (opts.history) {
4778
+ this.showHistory(opts);
4779
+ return;
4780
+ }
4781
+ if (opts.stats) {
4782
+ this.showStats(opts);
4783
+ return;
4784
+ }
4785
+ this.showSnapshot(opts);
4786
+ };
4787
+ get queryService() {
4788
+ return this.deps.queryService ?? llmUsageQueryService;
4789
+ }
4790
+ showSnapshot = (opts) => {
4791
+ const snapshot = this.queryService.getSnapshot();
4792
+ if (opts.json) {
4793
+ console.log(JSON.stringify({
4794
+ ok: Boolean(snapshot),
4795
+ mode: "snapshot",
4796
+ path: this.queryService.snapshotPath,
4797
+ snapshot
4798
+ }, null, 2));
4799
+ process.exitCode = 0;
4800
+ return;
4801
+ }
4802
+ if (!snapshot) {
4803
+ console.log([
4804
+ "No LLM usage snapshot recorded yet.",
4805
+ `Snapshot path: ${this.queryService.snapshotPath}`,
4806
+ "Run `nextclaw agent -m \"ping\"` or use the local UI once, then retry `nextclaw usage`."
4807
+ ].join("\n"));
4808
+ process.exitCode = 0;
4809
+ return;
4810
+ }
4811
+ console.log(this.renderSnapshot(snapshot));
4812
+ process.exitCode = 0;
4813
+ };
4814
+ showHistory = (opts) => {
4815
+ const records = this.queryService.getHistory(opts.limit);
4816
+ if (opts.json) {
4817
+ console.log(JSON.stringify({
4818
+ ok: records.length > 0,
4819
+ mode: "history",
4820
+ path: this.queryService.historyPath,
4821
+ limit: this.resolveLimit(opts.limit),
4822
+ records
4823
+ }, null, 2));
4824
+ process.exitCode = 0;
4825
+ return;
4826
+ }
4827
+ if (records.length === 0) {
4828
+ console.log([
4829
+ "No LLM usage history recorded yet.",
4830
+ `History path: ${this.queryService.historyPath}`,
4831
+ "Run `nextclaw agent -m \"ping\"` or use the local UI once, then retry `nextclaw usage --history`."
4832
+ ].join("\n"));
4833
+ process.exitCode = 0;
4834
+ return;
4835
+ }
4836
+ console.log(this.renderHistory(records));
4837
+ process.exitCode = 0;
4838
+ };
4839
+ showStats = (opts) => {
4840
+ const stats = this.queryService.getStats();
4841
+ if (opts.json) {
4842
+ console.log(JSON.stringify({
4843
+ ok: stats.totalRecords > 0,
4844
+ mode: "stats",
4845
+ path: this.queryService.historyPath,
4846
+ stats
4847
+ }, null, 2));
4848
+ process.exitCode = 0;
4849
+ return;
4850
+ }
4851
+ if (stats.totalRecords === 0) {
4852
+ console.log([
4853
+ "No LLM usage history recorded yet.",
4854
+ `History path: ${this.queryService.historyPath}`,
4855
+ "Run `nextclaw agent -m \"ping\"` or use the local UI once, then retry `nextclaw usage --stats`."
4856
+ ].join("\n"));
4857
+ process.exitCode = 0;
4858
+ return;
4859
+ }
4860
+ console.log(this.renderStats(stats));
4861
+ process.exitCode = 0;
4862
+ };
4863
+ renderSnapshot = (snapshot) => {
4864
+ const lines = [
4865
+ "Latest LLM usage snapshot",
4866
+ `Observed at: ${snapshot.observedAt}`,
4867
+ `Source: ${snapshot.source}`,
4868
+ `Model: ${snapshot.model ?? "unknown"}`,
4869
+ `Prompt tokens: ${snapshot.summary.promptTokens}`,
4870
+ `Completion tokens: ${snapshot.summary.completionTokens}`,
4871
+ `Total tokens: ${snapshot.summary.totalTokens}`,
4872
+ `Cached tokens: ${snapshot.summary.cachedTokens}`,
4873
+ `Cache hit: ${snapshot.summary.cacheHit ? "yes" : "no"}`,
4874
+ `Snapshot path: ${this.queryService.snapshotPath}`
4875
+ ];
4876
+ if (snapshot.summary.cacheMetricKeys.length > 0) lines.push(`Cache metric keys: ${snapshot.summary.cacheMetricKeys.join(", ")}`);
4877
+ if (Object.keys(snapshot.usage).length > 0) lines.push("", "Raw usage:", JSON.stringify(snapshot.usage, null, 2));
4878
+ return lines.join("\n");
4879
+ };
4880
+ renderHistory = (records) => {
4881
+ const lines = [
4882
+ "Recent LLM usage history",
4883
+ `History path: ${this.queryService.historyPath}`,
4884
+ `Showing: ${records.length} record(s)`,
4885
+ ""
4886
+ ];
4887
+ for (const [index, record] of records.entries()) lines.push(`${index + 1}. ${record.observedAt} | source=${record.source} | model=${record.model ?? "unknown"} | total=${record.summary.totalTokens} | cached=${record.summary.cachedTokens} | cache-hit=${record.summary.cacheHit ? "yes" : "no"}`);
4888
+ return lines.join("\n");
4889
+ };
4890
+ renderStats = (stats) => {
4891
+ const lines = [
4892
+ "LLM usage history stats",
4893
+ `History path: ${this.queryService.historyPath}`,
4894
+ `Records: ${stats.totalRecords}`,
4895
+ `Oldest observed at: ${stats.oldestObservedAt ?? "n/a"}`,
4896
+ `Latest observed at: ${stats.latestObservedAt ?? "n/a"}`,
4897
+ `Prompt tokens: ${stats.totalPromptTokens}`,
4898
+ `Completion tokens: ${stats.totalCompletionTokens}`,
4899
+ `Total tokens: ${stats.totalTokens}`,
4900
+ `Cached tokens: ${stats.totalCachedTokens}`,
4901
+ `Cache hits: ${stats.cacheHitRecords}/${stats.totalRecords} (${this.toPercent(stats.cacheHitRate)})`
4902
+ ];
4903
+ if (stats.sources.length > 0) lines.push(`Sources: ${stats.sources.map((item) => `${item.value}=${item.count}`).join(", ")}`);
4904
+ if (stats.models.length > 0) lines.push(`Models: ${stats.models.map((item) => `${item.value}=${item.count}`).join(", ")}`);
4905
+ return lines.join("\n");
4906
+ };
4907
+ resolveLimit = (value) => {
4908
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.floor(value);
4909
+ if (typeof value === "string" && value.trim().length > 0) {
4910
+ const parsed = Number(value);
4911
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
4912
+ }
4913
+ return 10;
4914
+ };
4915
+ toPercent = (value) => {
4916
+ return `${(value * 100).toFixed(1)}%`;
4917
+ };
4918
+ };
4919
+ //#endregion
4626
4920
  //#region src/cli/restart-coordinator.ts
4627
4921
  var RestartCoordinator = class {
4628
4922
  restartingService = false;
@@ -4851,7 +5145,7 @@ function parseSkillFrontmatter(raw) {
4851
5145
  const message = error instanceof Error ? error.message : String(error);
4852
5146
  throw new Error(`Invalid SKILL.md frontmatter: ${message}`);
4853
5147
  }
4854
- if (!isRecord$4(parsed)) return {};
5148
+ if (!isRecord$5(parsed)) return {};
4855
5149
  const summaryI18n = readLocalizedTextMapField(parsed, [["summaryi18n"], ["summary_i18n"]]);
4856
5150
  const descriptionI18n = readLocalizedTextMapField(parsed, [["descriptioni18n"], ["description_i18n"]]);
4857
5151
  const summaryZh = readFrontmatterStringField(parsed, [["summaryzh"], ["summary_zh"]]);
@@ -4882,7 +5176,7 @@ function readMarketplaceMetadataFile(skillDir, explicitMetaFile) {
4882
5176
  const message = error instanceof Error ? error.message : String(error);
4883
5177
  throw new Error(`Invalid marketplace metadata file: ${metadataPath} (${message})`);
4884
5178
  }
4885
- if (!isRecord$4(parsed)) throw new Error(`Invalid marketplace metadata file: ${metadataPath} (root must be an object)`);
5179
+ if (!isRecord$5(parsed)) throw new Error(`Invalid marketplace metadata file: ${metadataPath} (root must be an object)`);
4886
5180
  return {
4887
5181
  slug: readMetadataString(parsed, "slug"),
4888
5182
  name: readMetadataString(parsed, "name"),
@@ -4930,7 +5224,7 @@ function readMetadataStringArray(record, fieldName) {
4930
5224
  function readMetadataLocalizedTextMap(record, fieldName) {
4931
5225
  const value = record[fieldName];
4932
5226
  if (value == null) return;
4933
- if (!isRecord$4(value)) throw new Error(`Invalid marketplace metadata field: ${fieldName} must be an object`);
5227
+ if (!isRecord$5(value)) throw new Error(`Invalid marketplace metadata field: ${fieldName} must be an object`);
4934
5228
  const localized = {};
4935
5229
  for (const [locale, text] of Object.entries(value)) {
4936
5230
  if (typeof text !== "string") throw new Error(`Invalid marketplace metadata field: ${fieldName}.${locale} must be a string`);
@@ -4951,7 +5245,7 @@ function readFrontmatterStringField(record, keyPaths) {
4951
5245
  function readLocalizedTextMapField(record, keyPaths) {
4952
5246
  for (const keyPath of keyPaths) {
4953
5247
  const value = readNestedFrontmatterValue(record, keyPath);
4954
- if (!isRecord$4(value)) continue;
5248
+ if (!isRecord$5(value)) continue;
4955
5249
  const normalized = {};
4956
5250
  for (const [locale, text] of Object.entries(value)) {
4957
5251
  if (typeof text !== "string") continue;
@@ -4975,7 +5269,7 @@ function readFrontmatterTags(record) {
4975
5269
  function readNestedFrontmatterValue(record, keyPath) {
4976
5270
  let current = record;
4977
5271
  for (const rawKey of keyPath) {
4978
- if (!isRecord$4(current)) return;
5272
+ if (!isRecord$5(current)) return;
4979
5273
  const normalizedKey = normalizeFrontmatterKey(rawKey);
4980
5274
  const matchingKey = Object.keys(current).find((candidate) => normalizeFrontmatterKey(candidate) === normalizedKey);
4981
5275
  if (!matchingKey) return;
@@ -4989,7 +5283,7 @@ function normalizeFrontmatterKey(raw) {
4989
5283
  function normalizeLocaleTag(raw) {
4990
5284
  return raw.trim().toLowerCase();
4991
5285
  }
4992
- function isRecord$4(value) {
5286
+ function isRecord$5(value) {
4993
5287
  return typeof value === "object" && value !== null && !Array.isArray(value);
4994
5288
  }
4995
5289
  //#endregion
@@ -5130,7 +5424,10 @@ function resolveUiConfig(config, overrides) {
5130
5424
  };
5131
5425
  }
5132
5426
  function resolveUiApiBase(host, port) {
5133
- return `http://${host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host}:${port}`;
5427
+ return resolveLocalUiBaseUrl({
5428
+ host,
5429
+ port
5430
+ });
5134
5431
  }
5135
5432
  function isLoopbackHost(host) {
5136
5433
  const normalized = host.trim().toLowerCase();
@@ -5161,37 +5458,8 @@ async function resolvePublicIp(timeoutMs = 1500) {
5161
5458
  }
5162
5459
  return null;
5163
5460
  }
5164
- function readServiceState() {
5165
- const path = resolveServiceStatePath();
5166
- if (!existsSync(path)) return null;
5167
- try {
5168
- const raw = readFileSync(path, "utf-8");
5169
- return JSON.parse(raw);
5170
- } catch {
5171
- return null;
5172
- }
5173
- }
5174
- function writeServiceState(state) {
5175
- const path = resolveServiceStatePath();
5176
- mkdirSync(resolve(path, ".."), { recursive: true });
5177
- writeFileSync(path, JSON.stringify(state, null, 2));
5178
- }
5179
- function updateServiceState(updater) {
5180
- const current = readServiceState();
5181
- if (!current) return null;
5182
- const next = updater(current);
5183
- writeServiceState(next);
5184
- return next;
5185
- }
5186
- function clearServiceState() {
5187
- const path = resolveServiceStatePath();
5188
- if (existsSync(path)) rmSync(path, { force: true });
5189
- }
5190
- function resolveServiceStatePath() {
5191
- return resolve(getDataDir(), "run", "service.json");
5192
- }
5193
5461
  function resolveServiceLogPath() {
5194
- return resolve(getDataDir(), "logs", "service.log");
5462
+ return resolve(getLogsPath(), "service.log");
5195
5463
  }
5196
5464
  function isProcessRunning(pid) {
5197
5465
  try {
@@ -5584,9 +5852,9 @@ async function fetchMarketplaceSkillFiles(apiBase, slug) {
5584
5852
  const message = payload.error?.message || `marketplace skill file fetch failed: ${response.status}`;
5585
5853
  throw new Error(message);
5586
5854
  }
5587
- if (!isRecord$3(payload.data) || !Array.isArray(payload.data.files)) throw new Error("Invalid marketplace skill file manifest response");
5855
+ if (!isRecord$4(payload.data) || !Array.isArray(payload.data.files)) throw new Error("Invalid marketplace skill file manifest response");
5588
5856
  return { files: payload.data.files.map((entry, index) => {
5589
- if (!isRecord$3(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) throw new Error(`Invalid marketplace skill file manifest at index ${index}`);
5857
+ if (!isRecord$4(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) throw new Error(`Invalid marketplace skill file manifest at index ${index}`);
5590
5858
  const normalized = { path: entry.path.trim() };
5591
5859
  if (typeof entry.downloadPath === "string" && entry.downloadPath.trim().length > 0) normalized.downloadPath = entry.downloadPath.trim();
5592
5860
  if (typeof entry.contentBase64 === "string" && entry.contentBase64.trim().length > 0) normalized.contentBase64 = entry.contentBase64.trim();
@@ -5613,7 +5881,7 @@ async function readMarketplaceEnvelope(response) {
5613
5881
  } catch {
5614
5882
  throw new Error(`Invalid marketplace response: ${response.status}`);
5615
5883
  }
5616
- if (!isRecord$3(payload) || typeof payload.ok !== "boolean") throw new Error(`Invalid marketplace response shape: ${response.status}`);
5884
+ if (!isRecord$4(payload) || typeof payload.ok !== "boolean") throw new Error(`Invalid marketplace response shape: ${response.status}`);
5617
5885
  return payload;
5618
5886
  }
5619
5887
  function resolveSkillFileDownloadUrl(apiBase, slug, file) {
@@ -5628,7 +5896,7 @@ function extractMarketplaceErrorMessage(raw, fallbackStatus) {
5628
5896
  return raw || `Request failed (${fallbackStatus})`;
5629
5897
  }
5630
5898
  }
5631
- function isRecord$3(value) {
5899
+ function isRecord$4(value) {
5632
5900
  return typeof value === "object" && value !== null && !Array.isArray(value);
5633
5901
  }
5634
5902
  //#endregion
@@ -5936,6 +6204,41 @@ function decodeMarketplaceFileContent(path, contentBase64) {
5936
6204
  return Buffer.from(normalized, "base64");
5937
6205
  }
5938
6206
  //#endregion
6207
+ //#region src/cli/update/self-update-report.utils.ts
6208
+ function reportSelfUpdateResult(params) {
6209
+ const { appName, currentVersion, result, readInstalledVersion } = params;
6210
+ const printSteps = () => {
6211
+ for (const step of result.steps) {
6212
+ console.log(`- ${step.cmd} ${step.args.join(" ")} (code ${step.code ?? "?"})`);
6213
+ if (step.stderr) console.log(` stderr: ${step.stderr}`);
6214
+ if (step.stdout) console.log(` stdout: ${step.stdout}`);
6215
+ }
6216
+ };
6217
+ if (!result.ok) {
6218
+ console.error(`Update failed: ${result.error ?? "unknown error"}`);
6219
+ if (result.steps.length > 0) printSteps();
6220
+ return {
6221
+ ok: false,
6222
+ shouldSuggestRestart: false
6223
+ };
6224
+ }
6225
+ if (result.strategy === "noop") {
6226
+ console.log(`✓ ${appName} is already up to date (${result.latestVersion ?? currentVersion})`);
6227
+ return {
6228
+ ok: true,
6229
+ shouldSuggestRestart: false
6230
+ };
6231
+ }
6232
+ const versionAfter = result.latestVersion ?? readInstalledVersion();
6233
+ console.log(`✓ Update complete (${result.strategy})`);
6234
+ if (versionAfter === currentVersion) console.log(`Version unchanged: ${currentVersion}`);
6235
+ else console.log(`Version updated: ${currentVersion} -> ${versionAfter}`);
6236
+ return {
6237
+ ok: true,
6238
+ shouldSuggestRestart: true
6239
+ };
6240
+ }
6241
+ //#endregion
5939
6242
  //#region src/cli/update/runner.ts
5940
6243
  const DEFAULT_TIMEOUT_MS = 20 * 6e4;
5941
6244
  function runSelfUpdate(options = {}) {
@@ -5943,6 +6246,7 @@ function runSelfUpdate(options = {}) {
5943
6246
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
5944
6247
  const updateCommand = options.updateCommand ?? process.env.NEXTCLAW_UPDATE_COMMAND?.trim();
5945
6248
  const packageName = options.packageName ?? "nextclaw";
6249
+ const currentVersion = options.currentVersion?.trim() || null;
5946
6250
  const resolveShellCommand = (command) => {
5947
6251
  if (process.platform === "win32") return {
5948
6252
  cmd: process.env.ComSpec || "cmd.exe",
@@ -5966,19 +6270,33 @@ function runSelfUpdate(options = {}) {
5966
6270
  timeout: timeoutMs,
5967
6271
  stdio: "pipe"
5968
6272
  });
6273
+ const stdout = (result.stdout ?? "").toString().slice(0, 4e3);
6274
+ const stderr = (result.stderr ?? "").toString().slice(0, 4e3);
5969
6275
  steps.push({
5970
6276
  cmd,
5971
6277
  args,
5972
6278
  cwd,
5973
6279
  code: result.status,
5974
- stdout: (result.stdout ?? "").toString().slice(0, 4e3),
5975
- stderr: (result.stderr ?? "").toString().slice(0, 4e3)
6280
+ stdout,
6281
+ stderr
5976
6282
  });
5977
6283
  return {
5978
6284
  ok: result.status === 0,
5979
- code: result.status
6285
+ code: result.status,
6286
+ stdout,
6287
+ stderr
5980
6288
  };
5981
6289
  };
6290
+ const parseLatestVersion = (raw) => {
6291
+ const trimmed = raw.trim();
6292
+ if (!trimmed) return null;
6293
+ try {
6294
+ const parsed = JSON.parse(trimmed);
6295
+ return typeof parsed === "string" && parsed.trim() ? parsed.trim() : null;
6296
+ } catch {
6297
+ return trimmed;
6298
+ }
6299
+ };
5982
6300
  if (updateCommand) {
5983
6301
  const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
5984
6302
  const shellCommand = resolveShellCommand(updateCommand);
@@ -5996,19 +6314,35 @@ function runSelfUpdate(options = {}) {
5996
6314
  }
5997
6315
  const npmExecutable = findExecutableOnPath("npm");
5998
6316
  if (npmExecutable) {
6317
+ const cwd = options.cwd ? resolve(options.cwd) : process.cwd();
6318
+ const latestVersionStep = runStep(npmExecutable, [
6319
+ "view",
6320
+ packageName,
6321
+ "version",
6322
+ "--json"
6323
+ ], cwd);
6324
+ const latestVersion = latestVersionStep.ok ? parseLatestVersion(latestVersionStep.stdout) : null;
6325
+ if (latestVersion && currentVersion && latestVersion === currentVersion) return {
6326
+ ok: true,
6327
+ strategy: "noop",
6328
+ latestVersion,
6329
+ steps
6330
+ };
5999
6331
  if (!runStep(npmExecutable, [
6000
6332
  "i",
6001
6333
  "-g",
6002
6334
  packageName
6003
- ], process.cwd()).ok) return {
6335
+ ], cwd).ok) return {
6004
6336
  ok: false,
6005
6337
  error: `npm install -g ${packageName} failed`,
6006
6338
  strategy: "npm",
6339
+ latestVersion: latestVersion ?? void 0,
6007
6340
  steps
6008
6341
  };
6009
6342
  return {
6010
6343
  ok: true,
6011
6344
  strategy: "npm",
6345
+ latestVersion: latestVersion ?? void 0,
6012
6346
  steps
6013
6347
  };
6014
6348
  }
@@ -6020,6 +6354,40 @@ function runSelfUpdate(options = {}) {
6020
6354
  };
6021
6355
  }
6022
6356
  //#endregion
6357
+ //#region src/cli/runtime-state/managed-service-state.store.ts
6358
+ var ManagedServiceStateStore = class {
6359
+ get path() {
6360
+ return resolve(getDataDir(), "run", "service.json");
6361
+ }
6362
+ read = () => {
6363
+ if (!existsSync(this.path)) return null;
6364
+ try {
6365
+ const raw = readFileSync(this.path, "utf-8");
6366
+ return JSON.parse(raw);
6367
+ } catch {
6368
+ return null;
6369
+ }
6370
+ };
6371
+ write = (state) => {
6372
+ mkdirSync(resolve(this.path, ".."), { recursive: true });
6373
+ writeFileSync(this.path, JSON.stringify(state, null, 2));
6374
+ };
6375
+ update = (updater) => {
6376
+ const current = this.read();
6377
+ if (!current) return null;
6378
+ const next = updater(current);
6379
+ this.write(next);
6380
+ return next;
6381
+ };
6382
+ clear = () => {
6383
+ if (existsSync(this.path)) rmSync(this.path, { force: true });
6384
+ };
6385
+ clearIfOwnedByProcess = (pid = process.pid) => {
6386
+ if (this.read()?.pid === pid) this.clear();
6387
+ };
6388
+ };
6389
+ const managedServiceStateStore = new ManagedServiceStateStore();
6390
+ //#endregion
6023
6391
  //#region src/cli/commands/plugin/plugin-command-utils.ts
6024
6392
  const RESERVED_PROVIDER_IDS$1 = builtinProviderIds();
6025
6393
  const RESERVED_TOOL_NAMES = [
@@ -6045,7 +6413,6 @@ function buildReservedPluginLoadOptions() {
6045
6413
  reservedToolNames: [...RESERVED_TOOL_NAMES],
6046
6414
  reservedChannelIds: [],
6047
6415
  reservedProviderIds: RESERVED_PROVIDER_IDS$1,
6048
- reservedEngineKinds: ["native"],
6049
6416
  reservedNcpAgentRuntimeKinds: ["native"]
6050
6417
  };
6051
6418
  }
@@ -6053,11 +6420,10 @@ function appendPluginCapabilityLines(lines, plugin) {
6053
6420
  if (plugin.toolNames.length > 0) lines.push(`Tools: ${plugin.toolNames.join(", ")}`);
6054
6421
  if (plugin.channelIds.length > 0) lines.push(`Channels: ${plugin.channelIds.join(", ")}`);
6055
6422
  if (plugin.providerIds.length > 0) lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
6056
- if (plugin.engineKinds.length > 0) lines.push(`Engines: ${plugin.engineKinds.join(", ")}`);
6057
6423
  if (plugin.ncpAgentRuntimeKinds.length > 0) lines.push(`NCP runtimes: ${plugin.ncpAgentRuntimeKinds.join(", ")}`);
6058
6424
  }
6059
6425
  //#endregion
6060
- //#region src/cli/commands/plugin/dev-first-party-plugin-load-paths.ts
6426
+ //#region src/cli/commands/plugin/development-source/first-party-plugin-load-paths.ts
6061
6427
  const readJsonFile = (filePath) => {
6062
6428
  try {
6063
6429
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -6074,7 +6440,7 @@ const readString = (value) => {
6074
6440
  const resolveDevFirstPartyPluginDir = (explicitDir, moduleDir = path.dirname(fileURLToPath(import.meta.url))) => {
6075
6441
  const configured = explicitDir?.trim();
6076
6442
  if (configured) return configured;
6077
- const inferred = path.resolve(moduleDir, "../../../../extensions");
6443
+ const inferred = path.resolve(moduleDir, "../../../../../extensions");
6078
6444
  return fs.existsSync(inferred) ? inferred : void 0;
6079
6445
  };
6080
6446
  const hasOpenClawExtensions = (pkg) => {
@@ -6083,6 +6449,14 @@ const hasOpenClawExtensions = (pkg) => {
6083
6449
  const extensions = openclaw.extensions;
6084
6450
  return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.trim().length > 0);
6085
6451
  };
6452
+ const hasOpenClawDevelopmentExtensions = (pkg) => {
6453
+ const openclaw = pkg.openclaw;
6454
+ if (!openclaw || typeof openclaw !== "object" || Array.isArray(openclaw)) return false;
6455
+ const development = openclaw.development;
6456
+ if (!development || typeof development !== "object" || Array.isArray(development)) return false;
6457
+ const extensions = development.extensions;
6458
+ return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.trim().length > 0);
6459
+ };
6086
6460
  const normalizePackageSpec = (spec) => {
6087
6461
  const trimmed = spec.trim();
6088
6462
  if (!trimmed) return;
@@ -6108,11 +6482,38 @@ const readWorkspacePluginPackages = (workspaceExtensionsDir) => {
6108
6482
  if (!packageName?.startsWith("@nextclaw/")) continue;
6109
6483
  packages.push({
6110
6484
  packageName,
6111
- dir: packageDir
6485
+ dir: packageDir,
6486
+ supportsDevelopmentSource: hasOpenClawDevelopmentExtensions(pkg)
6112
6487
  });
6113
6488
  }
6114
6489
  return packages;
6115
6490
  };
6491
+ const mergeLoadPaths$1 = (existingLoadPaths, devLoadPaths) => {
6492
+ const mergedLoadPaths = [...devLoadPaths];
6493
+ for (const entry of existingLoadPaths) if (!mergedLoadPaths.includes(entry)) mergedLoadPaths.push(entry);
6494
+ return mergedLoadPaths;
6495
+ };
6496
+ const buildDevelopmentSourceEntryDefaults = (config, workspacePackages) => {
6497
+ const packageByName = new Map(workspacePackages.map((entry) => [entry.packageName, entry]));
6498
+ const nextEntries = { ...config.plugins.entries ?? {} };
6499
+ let didDefaultDevelopmentSource = false;
6500
+ for (const [pluginId, installRecord] of Object.entries(config.plugins.installs ?? {})) {
6501
+ const packageName = normalizePackageSpec(installRecord.spec ?? "");
6502
+ if (!packageName) continue;
6503
+ if (!packageByName.get(packageName)?.supportsDevelopmentSource) continue;
6504
+ const existingEntry = nextEntries[pluginId];
6505
+ if (existingEntry?.source) continue;
6506
+ nextEntries[pluginId] = {
6507
+ ...existingEntry,
6508
+ source: "development"
6509
+ };
6510
+ didDefaultDevelopmentSource = true;
6511
+ }
6512
+ return {
6513
+ didDefaultDevelopmentSource,
6514
+ nextEntries
6515
+ };
6516
+ };
6116
6517
  const resolveDevFirstPartyPluginLoadPaths = (config, workspaceExtensionsDir) => {
6117
6518
  const rootDir = resolveDevFirstPartyPluginDir(workspaceExtensionsDir);
6118
6519
  if (!rootDir) return [];
@@ -6147,15 +6548,19 @@ const resolveDevFirstPartyPluginInstallRoots = (config, workspaceExtensionsDir)
6147
6548
  return installRoots;
6148
6549
  };
6149
6550
  const applyDevFirstPartyPluginLoadPaths = (config, workspaceExtensionsDir) => {
6150
- const devLoadPaths = resolveDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir);
6551
+ const rootDir = resolveDevFirstPartyPluginDir(workspaceExtensionsDir);
6552
+ if (!rootDir) return config;
6553
+ const workspacePackages = readWorkspacePluginPackages(rootDir);
6554
+ if (workspacePackages.length === 0) return config;
6555
+ const devLoadPaths = resolveDevFirstPartyPluginLoadPaths(config, rootDir);
6151
6556
  if (devLoadPaths.length === 0) return config;
6152
- const existingLoadPaths = Array.isArray(config.plugins.load?.paths) ? config.plugins.load.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
6153
- const mergedLoadPaths = [...devLoadPaths];
6154
- for (const entry of existingLoadPaths) if (!mergedLoadPaths.includes(entry)) mergedLoadPaths.push(entry);
6557
+ const mergedLoadPaths = mergeLoadPaths$1(Array.isArray(config.plugins.load?.paths) ? config.plugins.load.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [], devLoadPaths);
6558
+ const { didDefaultDevelopmentSource, nextEntries } = buildDevelopmentSourceEntryDefaults(config, workspacePackages);
6155
6559
  return {
6156
6560
  ...config,
6157
6561
  plugins: {
6158
6562
  ...config.plugins,
6563
+ entries: didDefaultDevelopmentSource ? nextEntries : config.plugins.entries,
6159
6564
  load: {
6160
6565
  ...config.plugins.load,
6161
6566
  paths: mergedLoadPaths
@@ -6164,6 +6569,112 @@ const applyDevFirstPartyPluginLoadPaths = (config, workspaceExtensionsDir) => {
6164
6569
  };
6165
6570
  };
6166
6571
  //#endregion
6572
+ //#region src/cli/commands/plugin/development-source/dev-plugin-overrides.utils.ts
6573
+ const DEV_PLUGIN_OVERRIDES_ENV = "NEXTCLAW_DEV_PLUGIN_OVERRIDES";
6574
+ function isRecord$3(value) {
6575
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6576
+ }
6577
+ function readOptionalString$6(value) {
6578
+ if (typeof value !== "string") return;
6579
+ return value.trim() || void 0;
6580
+ }
6581
+ function readPackageManifest(pluginPath) {
6582
+ const packageJsonPath = path.join(pluginPath, "package.json");
6583
+ if (!fs.existsSync(packageJsonPath)) return null;
6584
+ try {
6585
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
6586
+ } catch {
6587
+ return null;
6588
+ }
6589
+ }
6590
+ function assertOverridePluginReadable(override) {
6591
+ if (!fs.existsSync(override.pluginPath)) throw new Error(`[dev-plugin-override] plugin path does not exist for "${override.pluginId}": ${override.pluginPath}`);
6592
+ const packageManifest = readPackageManifest(override.pluginPath);
6593
+ if (!packageManifest) throw new Error(`[dev-plugin-override] package.json is missing or invalid for "${override.pluginId}": ${override.pluginPath}`);
6594
+ const pluginManifest = loadPluginManifest(override.pluginPath);
6595
+ if (!pluginManifest.ok) throw new Error(`[dev-plugin-override] ${pluginManifest.error} for "${override.pluginId}": ${override.pluginPath}`);
6596
+ if (pluginManifest.manifest.id !== override.pluginId) throw new Error(`[dev-plugin-override] plugin id mismatch: expected "${override.pluginId}" but found "${pluginManifest.manifest.id}" at ${override.pluginPath}`);
6597
+ if (getPackageManifestExtensions(packageManifest, override.source).length === 0) {
6598
+ const missingEntry = override.source === "development" ? "openclaw.development.extensions" : "openclaw.extensions";
6599
+ throw new Error(`[dev-plugin-override] ${missingEntry} is missing for "${override.pluginId}" at ${override.pluginPath}`);
6600
+ }
6601
+ }
6602
+ function readOverrideRecord(value, index) {
6603
+ if (!isRecord$3(value)) throw new Error(`[dev-plugin-override] override[${index}] must be an object`);
6604
+ const pluginId = readOptionalString$6(value.pluginId);
6605
+ const pluginPath = readOptionalString$6(value.pluginPath);
6606
+ const source = value.source === "development" ? "development" : "production";
6607
+ if (!pluginId || !pluginPath) throw new Error(`[dev-plugin-override] override[${index}] requires pluginId and pluginPath`);
6608
+ const normalized = {
6609
+ pluginId,
6610
+ pluginPath: path.resolve(pluginPath),
6611
+ source
6612
+ };
6613
+ assertOverridePluginReadable(normalized);
6614
+ return normalized;
6615
+ }
6616
+ function resolveDevPluginOverrides(rawEnv = process.env[DEV_PLUGIN_OVERRIDES_ENV]) {
6617
+ if (typeof rawEnv !== "string" || rawEnv.trim().length === 0) return [];
6618
+ let parsed;
6619
+ try {
6620
+ parsed = JSON.parse(rawEnv);
6621
+ } catch (error) {
6622
+ throw new Error(`[dev-plugin-override] failed to parse ${DEV_PLUGIN_OVERRIDES_ENV}: ${error instanceof Error ? error.message : String(error)}`);
6623
+ }
6624
+ if (!Array.isArray(parsed)) throw new Error(`[dev-plugin-override] ${DEV_PLUGIN_OVERRIDES_ENV} must be a JSON array`);
6625
+ const seenPluginIds = /* @__PURE__ */ new Set();
6626
+ const overrides = parsed.map((entry, index) => readOverrideRecord(entry, index));
6627
+ for (const entry of overrides) {
6628
+ if (seenPluginIds.has(entry.pluginId)) throw new Error(`[dev-plugin-override] duplicate plugin override for "${entry.pluginId}"`);
6629
+ seenPluginIds.add(entry.pluginId);
6630
+ }
6631
+ return overrides;
6632
+ }
6633
+ function mergeLoadPaths(existingLoadPaths, overrideLoadPaths) {
6634
+ const merged = [...overrideLoadPaths];
6635
+ for (const entry of existingLoadPaths) if (!merged.includes(entry)) merged.push(entry);
6636
+ return merged;
6637
+ }
6638
+ function applyExplicitDevPluginOverrides(config, overrides) {
6639
+ if (overrides.length === 0) return config;
6640
+ const nextEntries = { ...config.plugins.entries ?? {} };
6641
+ for (const override of overrides) nextEntries[override.pluginId] = {
6642
+ ...nextEntries[override.pluginId] ?? {},
6643
+ source: override.source
6644
+ };
6645
+ const existingLoadPaths = Array.isArray(config.plugins.load?.paths) ? config.plugins.load.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
6646
+ return {
6647
+ ...config,
6648
+ plugins: {
6649
+ ...config.plugins,
6650
+ entries: nextEntries,
6651
+ load: {
6652
+ ...config.plugins.load,
6653
+ paths: mergeLoadPaths(existingLoadPaths, overrides.map((entry) => entry.pluginPath))
6654
+ }
6655
+ }
6656
+ };
6657
+ }
6658
+ function resolveDevPluginOverrideInstallRoots(config, overrides) {
6659
+ const installRoots = [];
6660
+ for (const override of overrides) {
6661
+ const installRecord = config.plugins.installs?.[override.pluginId];
6662
+ const installPath = readOptionalString$6(installRecord?.installPath);
6663
+ if (!installPath || installRoots.includes(installPath)) continue;
6664
+ installRoots.push(installPath);
6665
+ }
6666
+ return installRoots;
6667
+ }
6668
+ function resolveDevPluginLoadingContext(config, workspaceExtensionsDir, rawOverridesEnv = process.env[DEV_PLUGIN_OVERRIDES_ENV]) {
6669
+ const configWithFirstPartyOverrides = applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir);
6670
+ const overrides = resolveDevPluginOverrides(rawOverridesEnv);
6671
+ return {
6672
+ configWithDevPluginOverrides: applyExplicitDevPluginOverrides(configWithFirstPartyOverrides, overrides),
6673
+ excludedRoots: [...resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir), ...resolveDevPluginOverrideInstallRoots(config, overrides)].filter((entry, index, list) => list.indexOf(entry) === index),
6674
+ overrides
6675
+ };
6676
+ }
6677
+ //#endregion
6167
6678
  //#region src/cli/commands/plugin/plugin-mutation-actions.ts
6168
6679
  const pluginInstallLogger = {
6169
6680
  info: (message) => console.log(message),
@@ -6335,12 +6846,6 @@ function toExtensionRegistry(pluginRegistry) {
6335
6846
  channel: channel.channel,
6336
6847
  source: channel.source
6337
6848
  })),
6338
- engines: pluginRegistry.engines.map((engine) => ({
6339
- extensionId: engine.pluginId,
6340
- kind: engine.kind,
6341
- factory: engine.factory,
6342
- source: engine.source
6343
- })),
6344
6849
  ncpAgentRuntimes: pluginRegistry.ncpAgentRuntimes.map((runtime) => ({
6345
6850
  pluginId: runtime.pluginId,
6346
6851
  kind: runtime.kind,
@@ -6360,11 +6865,11 @@ function toExtensionRegistry(pluginRegistry) {
6360
6865
  //#endregion
6361
6866
  //#region src/cli/commands/plugins.ts
6362
6867
  function loadPluginRegistry(config, workspaceDir) {
6363
- const workspaceExtensionsDir = resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR);
6868
+ const { configWithDevPluginOverrides, excludedRoots } = resolveDevPluginLoadingContext(config, resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR));
6364
6869
  return loadOpenClawPlugins({
6365
- config: applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir),
6870
+ config: configWithDevPluginOverrides,
6366
6871
  workspaceDir,
6367
- excludeRoots: resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir),
6872
+ excludeRoots: excludedRoots,
6368
6873
  ...buildReservedPluginLoadOptions(),
6369
6874
  logger: {
6370
6875
  info: (message) => console.log(message),
@@ -6894,7 +7399,7 @@ var ConfigCommands = class {
6894
7399
  };
6895
7400
  //#endregion
6896
7401
  //#region src/cli/commands/mcp.ts
6897
- function normalizeOptionalString$5(value) {
7402
+ function normalizeOptionalString$7(value) {
6898
7403
  if (typeof value !== "string") return;
6899
7404
  return value.trim() || void 0;
6900
7405
  }
@@ -6917,9 +7422,9 @@ function parseTimeoutMs$1(value) {
6917
7422
  return Math.trunc(parsed);
6918
7423
  }
6919
7424
  function buildMcpServerDefinition(command, opts) {
6920
- const transport = (normalizeOptionalString$5(opts.transport) ?? "stdio").toLowerCase();
7425
+ const transport = (normalizeOptionalString$7(opts.transport) ?? "stdio").toLowerCase();
6921
7426
  const disabled = Boolean(opts.disabled);
6922
- const explicitAgents = Array.from(new Set((opts.agent ?? []).map((agentId) => normalizeOptionalString$5(agentId)).filter((agentId) => Boolean(agentId))));
7427
+ const explicitAgents = Array.from(new Set((opts.agent ?? []).map((agentId) => normalizeOptionalString$7(agentId)).filter((agentId) => Boolean(agentId))));
6923
7428
  const allAgents = explicitAgents.length === 0 ? true : Boolean(opts.allAgents);
6924
7429
  if (transport === "stdio") {
6925
7430
  if (command.length === 0) throw new Error("stdio transport requires a command after --");
@@ -6929,9 +7434,9 @@ function buildMcpServerDefinition(command, opts) {
6929
7434
  type: "stdio",
6930
7435
  command: command[0],
6931
7436
  args: command.slice(1),
6932
- cwd: normalizeOptionalString$5(opts.cwd),
7437
+ cwd: normalizeOptionalString$7(opts.cwd),
6933
7438
  env: parsePairs(opts.env, "env"),
6934
- stderr: normalizeOptionalString$5(opts.stderr) ?? "pipe"
7439
+ stderr: normalizeOptionalString$7(opts.stderr) ?? "pipe"
6935
7440
  },
6936
7441
  scope: {
6937
7442
  allAgents,
@@ -6947,7 +7452,7 @@ function buildMcpServerDefinition(command, opts) {
6947
7452
  }
6948
7453
  };
6949
7454
  }
6950
- const url = normalizeOptionalString$5(opts.url);
7455
+ const url = normalizeOptionalString$7(opts.url);
6951
7456
  if (!url) throw new Error(`${transport} transport requires --url`);
6952
7457
  const timeoutMs = parseTimeoutMs$1(opts.timeoutMs);
6953
7458
  const shared = {
@@ -7094,7 +7599,7 @@ function normalizeSecretSource(value) {
7094
7599
  const normalized = value.trim().toLowerCase();
7095
7600
  return SECRET_SOURCES.includes(normalized) ? normalized : null;
7096
7601
  }
7097
- function normalizeOptionalString$4(value) {
7602
+ function normalizeOptionalString$6(value) {
7098
7603
  if (typeof value !== "string") return;
7099
7604
  return value.trim() || void 0;
7100
7605
  }
@@ -7105,9 +7610,9 @@ function parseTimeoutMs(value) {
7105
7610
  return Math.trunc(parsed);
7106
7611
  }
7107
7612
  function inferProviderAlias(config, ref) {
7108
- const explicit = normalizeOptionalString$4(ref.provider);
7613
+ const explicit = normalizeOptionalString$6(ref.provider);
7109
7614
  if (explicit) return explicit;
7110
- const defaultAlias = normalizeOptionalString$4(config.secrets.defaults[ref.source]);
7615
+ const defaultAlias = normalizeOptionalString$6(config.secrets.defaults[ref.source]);
7111
7616
  if (defaultAlias) return defaultAlias;
7112
7617
  return ref.source;
7113
7618
  }
@@ -7125,8 +7630,8 @@ function parseRefsPatch(raw) {
7125
7630
  for (const [path, value] of Object.entries(raw)) {
7126
7631
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`invalid ref for ${path}`);
7127
7632
  const source = normalizeSecretSource(value.source);
7128
- const id = normalizeOptionalString$4(value.id);
7129
- const provider = normalizeOptionalString$4(value.provider);
7633
+ const id = normalizeOptionalString$6(value.id);
7634
+ const provider = normalizeOptionalString$6(value.provider);
7130
7635
  if (!source || !id) throw new Error(`invalid ref for ${path}: source/id is required`);
7131
7636
  output[path] = {
7132
7637
  source,
@@ -7213,7 +7718,7 @@ var SecretsCommands = class {
7213
7718
  if (Boolean(opts.strict) && summary.failed > 0) process.exitCode = 1;
7214
7719
  }
7215
7720
  async secretsConfigure(opts) {
7216
- const alias = normalizeOptionalString$4(opts.provider);
7721
+ const alias = normalizeOptionalString$6(opts.provider);
7217
7722
  if (!alias) throw new Error("provider alias is required");
7218
7723
  const prevConfig = loadConfig();
7219
7724
  const nextConfig = structuredClone(prevConfig);
@@ -7226,10 +7731,10 @@ var SecretsCommands = class {
7226
7731
  if (!source) throw new Error("source is required and must be one of env/file/exec");
7227
7732
  if (source === "env") nextConfig.secrets.providers[alias] = {
7228
7733
  source,
7229
- ...normalizeOptionalString$4(opts.prefix) ? { prefix: normalizeOptionalString$4(opts.prefix) } : {}
7734
+ ...normalizeOptionalString$6(opts.prefix) ? { prefix: normalizeOptionalString$6(opts.prefix) } : {}
7230
7735
  };
7231
7736
  else if (source === "file") {
7232
- const path = normalizeOptionalString$4(opts.path);
7737
+ const path = normalizeOptionalString$6(opts.path);
7233
7738
  if (!path) throw new Error("file source requires --path");
7234
7739
  nextConfig.secrets.providers[alias] = {
7235
7740
  source,
@@ -7237,13 +7742,13 @@ var SecretsCommands = class {
7237
7742
  format: "json"
7238
7743
  };
7239
7744
  } else {
7240
- const command = normalizeOptionalString$4(opts.command);
7745
+ const command = normalizeOptionalString$6(opts.command);
7241
7746
  if (!command) throw new Error("exec source requires --command");
7242
7747
  nextConfig.secrets.providers[alias] = {
7243
7748
  source,
7244
7749
  command,
7245
7750
  args: Array.isArray(opts.arg) ? opts.arg : [],
7246
- ...normalizeOptionalString$4(opts.cwd) ? { cwd: normalizeOptionalString$4(opts.cwd) } : {},
7751
+ ...normalizeOptionalString$6(opts.cwd) ? { cwd: normalizeOptionalString$6(opts.cwd) } : {},
7247
7752
  timeoutMs: parseTimeoutMs(opts.timeoutMs) ?? 5e3
7248
7753
  };
7249
7754
  }
@@ -7288,9 +7793,9 @@ var SecretsCommands = class {
7288
7793
  if (opts.remove) delete nextConfig.secrets.refs[path];
7289
7794
  else {
7290
7795
  const source = normalizeSecretSource(opts.source);
7291
- const id = normalizeOptionalString$4(opts.id);
7796
+ const id = normalizeOptionalString$6(opts.id);
7292
7797
  if (!source || !id) throw new Error("apply single ref requires --source and --id");
7293
- const provider = normalizeOptionalString$4(opts.provider);
7798
+ const provider = normalizeOptionalString$6(opts.provider);
7294
7799
  nextConfig.secrets.refs[path] = {
7295
7800
  source,
7296
7801
  id,
@@ -7636,34 +8141,114 @@ function printCronJobs(jobs) {
7636
8141
  for (const job of jobs) console.log(`${job.id} [${job.enabled ? "enabled" : "disabled"}] ${job.name} ${formatCronSchedule(job.schedule)}`);
7637
8142
  }
7638
8143
  //#endregion
7639
- //#region src/cli/commands/shared/ui-bridge-api.service.ts
7640
- function resolveManagedApiBase() {
7641
- const state = readServiceState();
7642
- if (!state?.pid) return null;
7643
- if (!isProcessRunning(state.pid)) return null;
7644
- if (typeof state.uiUrl === "string" && state.uiUrl.trim().length > 0) return state.uiUrl.replace(/\/+$/, "");
7645
- if (typeof state.apiUrl === "string" && state.apiUrl.trim().length > 0) return state.apiUrl.replace(/\/api\/?$/, "").replace(/\/+$/, "");
7646
- return null;
7647
- }
7648
- var UiBridgeApiClient = class {
7649
- cookie;
7650
- constructor(apiBase) {
7651
- this.apiBase = apiBase;
8144
+ //#region src/cli/runtime-state/local-ui-runtime.store.ts
8145
+ var LocalUiRuntimeStore = class {
8146
+ get path() {
8147
+ return resolve(getDataDir(), "run", "ui-runtime.json");
7652
8148
  }
7653
- getCookie = async () => {
7654
- if (this.cookie !== void 0) return this.cookie;
7655
- const bridgeSecret = ensureUiBridgeSecret();
7656
- const response = await fetch(`${this.apiBase}/api/auth/bridge`, {
7657
- method: "POST",
7658
- headers: { "x-nextclaw-ui-bridge-secret": bridgeSecret }
7659
- });
7660
- if (!response.ok) throw new Error(`bridge auth failed with status ${response.status}`);
7661
- const payload = await response.json();
7662
- if (!payload.ok) throw new Error(payload.error?.message ?? "bridge auth failed");
7663
- this.cookie = typeof payload.data.cookie === "string" && payload.data.cookie.trim() ? payload.data.cookie.trim() : null;
7664
- return this.cookie;
8149
+ read = () => {
8150
+ if (!existsSync(this.path)) return null;
8151
+ try {
8152
+ const raw = readFileSync(this.path, "utf-8");
8153
+ return JSON.parse(raw);
8154
+ } catch {
8155
+ return null;
8156
+ }
7665
8157
  };
7666
- request = async (params) => {
8158
+ write = (state) => {
8159
+ mkdirSync(resolve(this.path, ".."), { recursive: true });
8160
+ writeFileSync(this.path, JSON.stringify(state, null, 2));
8161
+ };
8162
+ update = (updater) => {
8163
+ const current = this.read();
8164
+ if (!current) return null;
8165
+ const next = updater(current);
8166
+ this.write(next);
8167
+ return next;
8168
+ };
8169
+ clear = () => {
8170
+ if (existsSync(this.path)) rmSync(this.path, { force: true });
8171
+ };
8172
+ clearIfOwnedByProcess = (pid = process.pid) => {
8173
+ if (this.read()?.pid === pid) this.clear();
8174
+ };
8175
+ writeCurrentProcess = (uiConfig, pid = process.pid) => {
8176
+ const existing = this.read();
8177
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
8178
+ const state = {
8179
+ pid,
8180
+ startedAt: existing?.pid === pid && typeof existing.startedAt === "string" ? existing.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
8181
+ uiUrl,
8182
+ apiUrl: `${uiUrl}/api`,
8183
+ uiHost: uiConfig.host,
8184
+ uiPort: uiConfig.port,
8185
+ ...existing?.remote ? { remote: existing.remote } : {}
8186
+ };
8187
+ this.write(state);
8188
+ return state;
8189
+ };
8190
+ };
8191
+ const localUiRuntimeStore = new LocalUiRuntimeStore();
8192
+ //#endregion
8193
+ //#region src/cli/runtime-state/local-ui-discovery.service.ts
8194
+ var LocalUiDiscoveryService = class {
8195
+ constructor(localUiStore = localUiRuntimeStore, managedServiceStore = managedServiceStateStore, isProcessRunningFn = (pid) => isProcessRunning(pid)) {
8196
+ this.localUiStore = localUiStore;
8197
+ this.managedServiceStore = managedServiceStore;
8198
+ this.isProcessRunningFn = isProcessRunningFn;
8199
+ }
8200
+ readRunningState = (state) => {
8201
+ if (!state || !this.isProcessRunningFn(state.pid)) return null;
8202
+ return state;
8203
+ };
8204
+ readRunningRuntimeState = () => {
8205
+ return this.readRunningState(this.localUiStore.read()) ?? this.readRunningState(this.managedServiceStore.read());
8206
+ };
8207
+ resolveApiBase = () => {
8208
+ const state = this.readRunningRuntimeState();
8209
+ if (!state) return null;
8210
+ if (typeof state.uiUrl === "string" && state.uiUrl.trim().length > 0) return state.uiUrl.replace(/\/+$/, "");
8211
+ if (typeof state.apiUrl === "string" && state.apiUrl.trim().length > 0) return state.apiUrl.replace(/\/api\/?$/, "").replace(/\/+$/, "");
8212
+ return null;
8213
+ };
8214
+ resolveLocalOrigin = (config) => {
8215
+ const state = this.readRunningRuntimeState();
8216
+ const runtimePort = state && typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : null;
8217
+ if (runtimePort !== null) return resolveLocalUiBaseUrl({
8218
+ host: "0.0.0.0",
8219
+ port: runtimePort
8220
+ });
8221
+ return resolveLocalUiBaseUrl({
8222
+ host: "0.0.0.0",
8223
+ port: typeof config.ui.port === "number" && Number.isFinite(config.ui.port) ? config.ui.port : 55667
8224
+ });
8225
+ };
8226
+ };
8227
+ const localUiDiscoveryService = new LocalUiDiscoveryService();
8228
+ //#endregion
8229
+ //#region src/cli/commands/shared/ui-bridge-api.service.ts
8230
+ function resolveLocalUiApiBase() {
8231
+ return localUiDiscoveryService.resolveApiBase();
8232
+ }
8233
+ var UiBridgeApiClient = class {
8234
+ cookie;
8235
+ constructor(apiBase) {
8236
+ this.apiBase = apiBase;
8237
+ }
8238
+ getCookie = async () => {
8239
+ if (this.cookie !== void 0) return this.cookie;
8240
+ const bridgeSecret = ensureUiBridgeSecret();
8241
+ const response = await fetch(`${this.apiBase}/api/auth/bridge`, {
8242
+ method: "POST",
8243
+ headers: { "x-nextclaw-ui-bridge-secret": bridgeSecret }
8244
+ });
8245
+ if (!response.ok) throw new Error(`bridge auth failed with status ${response.status}`);
8246
+ const payload = await response.json();
8247
+ if (!payload.ok) throw new Error(payload.error?.message ?? "bridge auth failed");
8248
+ this.cookie = typeof payload.data.cookie === "string" && payload.data.cookie.trim() ? payload.data.cookie.trim() : null;
8249
+ return this.cookie;
8250
+ };
8251
+ request = async (params) => {
7667
8252
  const cookie = await this.getCookie();
7668
8253
  const response = await fetch(`${this.apiBase}${params.path}`, {
7669
8254
  method: params.method ?? "GET",
@@ -7690,7 +8275,7 @@ var CronCommands = class {
7690
8275
  this.local = local;
7691
8276
  }
7692
8277
  createApiClient = () => {
7693
- const apiBase = resolveManagedApiBase();
8278
+ const apiBase = resolveLocalUiApiBase();
7694
8279
  if (!apiBase) return null;
7695
8280
  return new UiBridgeApiClient(apiBase);
7696
8281
  };
@@ -7857,13 +8442,13 @@ function createUnusedRuntime(_params) {
7857
8442
  throw new Error("runtime creation is not available during runtime listing");
7858
8443
  }
7859
8444
  function loadRuntimeOnlyPluginRegistry(config, workspaceDir) {
7860
- const workspaceExtensionsDir = resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR);
8445
+ const { configWithDevPluginOverrides, excludedRoots } = resolveDevPluginLoadingContext(config, resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR));
7861
8446
  return loadOpenClawPlugins({
7862
- config: applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir),
8447
+ config: configWithDevPluginOverrides,
7863
8448
  workspaceDir,
7864
8449
  includeBundled: false,
7865
8450
  kinds: ["agent-runtime"],
7866
- excludeRoots: resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir),
8451
+ excludeRoots: excludedRoots,
7867
8452
  ...buildReservedPluginLoadOptions(),
7868
8453
  logger: {
7869
8454
  info: (message) => console.log(message),
@@ -8025,7 +8610,7 @@ var AgentCommands = class {
8025
8610
  //#region src/cli/commands/remote-support/remote-runtime-support.ts
8026
8611
  let currentProcessRemoteRuntimeState = null;
8027
8612
  function hasRunningNextclawManagedService() {
8028
- const state = readServiceState();
8613
+ const state = managedServiceStateStore.read();
8029
8614
  return Boolean(state && isProcessRunning(state.pid));
8030
8615
  }
8031
8616
  function createNextclawRemotePlatformClient() {
@@ -8038,7 +8623,7 @@ function createNextclawRemotePlatformClient() {
8038
8623
  requireConfigured: true
8039
8624
  }).platformBase,
8040
8625
  readManagedServiceState: () => {
8041
- const state = readServiceState();
8626
+ const state = managedServiceStateStore.read();
8042
8627
  if (!state) return null;
8043
8628
  return {
8044
8629
  pid: state.pid,
@@ -8057,9 +8642,13 @@ function createNextclawRemoteConnector(params = {}) {
8057
8642
  function createNextclawRemoteStatusStore(mode) {
8058
8643
  return new RemoteStatusStore(mode, { writeRemoteState: (next) => {
8059
8644
  currentProcessRemoteRuntimeState = next;
8060
- const serviceState = readServiceState();
8645
+ if (localUiRuntimeStore.read()?.pid === process.pid) localUiRuntimeStore.update((state) => ({
8646
+ ...state,
8647
+ remote: next
8648
+ }));
8649
+ const serviceState = managedServiceStateStore.read();
8061
8650
  if (!serviceState || serviceState.pid !== process.pid) return;
8062
- updateServiceState((state) => ({
8651
+ managedServiceStateStore.update((state) => ({
8063
8652
  ...state,
8064
8653
  remote: next
8065
8654
  }));
@@ -8069,10 +8658,12 @@ function buildNextclawConfiguredRemoteState(config) {
8069
8658
  return buildConfiguredRemoteState(config);
8070
8659
  }
8071
8660
  function readCurrentNextclawRemoteRuntimeState() {
8072
- const serviceState = readServiceState();
8073
- const currentRemoteState = currentProcessRemoteRuntimeState ?? serviceState?.remote ?? null;
8661
+ const uiRuntimeState = localUiRuntimeStore.read();
8662
+ const serviceState = managedServiceStateStore.read();
8663
+ const currentRemoteState = currentProcessRemoteRuntimeState ?? uiRuntimeState?.remote ?? serviceState?.remote ?? null;
8074
8664
  if (!currentRemoteState) return null;
8075
- if (!serviceState || isProcessRunning(serviceState.pid)) return currentRemoteState;
8665
+ const owningRuntime = uiRuntimeState ?? serviceState;
8666
+ if (!owningRuntime || isProcessRunning(owningRuntime.pid)) return currentRemoteState;
8076
8667
  return {
8077
8668
  ...currentRemoteState,
8078
8669
  state: currentRemoteState.enabled ? "disconnected" : "disabled",
@@ -8088,15 +8679,13 @@ function resolveNextclawRemoteStatusSnapshot(config) {
8088
8679
  }
8089
8680
  //#endregion
8090
8681
  //#region src/cli/commands/remote.ts
8091
- function normalizeOptionalString$3(value) {
8682
+ function normalizeOptionalString$5(value) {
8092
8683
  if (typeof value !== "string") return;
8093
8684
  const trimmed = value.trim();
8094
8685
  return trimmed.length > 0 ? trimmed : void 0;
8095
8686
  }
8096
8687
  function resolveConfiguredLocalOrigin(config) {
8097
- const state = readServiceState();
8098
- if (state && isProcessRunning(state.pid) && Number.isFinite(state.uiPort)) return `http://127.0.0.1:${state.uiPort}`;
8099
- return `http://127.0.0.1:${typeof config.ui.port === "number" && Number.isFinite(config.ui.port) ? config.ui.port : 55667}`;
8688
+ return localUiDiscoveryService.resolveLocalOrigin(config);
8100
8689
  }
8101
8690
  async function probeLocalUi(localOrigin) {
8102
8691
  try {
@@ -8187,8 +8776,8 @@ var RemoteCommands = class {
8187
8776
  configuredEnabled: snapshot.configuredEnabled,
8188
8777
  runtime: snapshot.runtime,
8189
8778
  localOrigin: resolvedLocalOrigin,
8190
- deviceName: snapshot.runtime?.deviceName ?? normalizeOptionalString$3(config.remote.deviceName) ?? hostname(),
8191
- platformBase: snapshot.runtime?.platformBase ?? normalizeOptionalString$3(config.remote.platformApiBase) ?? normalizeOptionalString$3(config.providers.nextclaw?.apiBase) ?? null
8779
+ deviceName: snapshot.runtime?.deviceName ?? normalizeOptionalString$5(config.remote.deviceName) ?? hostname(),
8780
+ platformBase: snapshot.runtime?.platformBase ?? normalizeOptionalString$5(config.remote.platformApiBase) ?? normalizeOptionalString$5(config.providers.nextclaw?.apiBase) ?? null
8192
8781
  };
8193
8782
  }
8194
8783
  async status(opts = {}) {
@@ -8214,8 +8803,8 @@ var RemoteCommands = class {
8214
8803
  const snapshot = resolveNextclawRemoteStatusSnapshot(config);
8215
8804
  const localOrigin = snapshot.runtime?.localOrigin ?? this.deps.currentLocalOrigin ?? resolveConfiguredLocalOrigin(config);
8216
8805
  const localUi = await probeLocalUi(localOrigin);
8217
- const token = normalizeOptionalString$3(config.providers.nextclaw?.apiKey);
8218
- const platformApiBase = normalizeOptionalString$3(config.remote.platformApiBase) ?? normalizeOptionalString$3(config.providers.nextclaw?.apiBase);
8806
+ const token = normalizeOptionalString$5(config.providers.nextclaw?.apiKey);
8807
+ const platformApiBase = normalizeOptionalString$5(config.remote.platformApiBase) ?? normalizeOptionalString$5(config.providers.nextclaw?.apiBase);
8219
8808
  const checks = [
8220
8809
  {
8221
8810
  name: "remote-enabled",
@@ -8324,7 +8913,7 @@ var DiagnosticsCommands = class {
8324
8913
  constructor(deps) {
8325
8914
  this.deps = deps;
8326
8915
  }
8327
- async status(opts = {}) {
8916
+ status = async (opts = {}) => {
8328
8917
  const report = await this.collectRuntimeStatus({
8329
8918
  verbose: Boolean(opts.verbose),
8330
8919
  fix: Boolean(opts.fix)
@@ -8340,25 +8929,53 @@ var DiagnosticsCommands = class {
8340
8929
  verbose: Boolean(opts.verbose)
8341
8930
  });
8342
8931
  process.exitCode = 0;
8343
- }
8344
- async doctor(opts = {}) {
8932
+ };
8933
+ doctor = async (opts = {}) => {
8345
8934
  const report = await this.collectRuntimeStatus({
8346
8935
  verbose: Boolean(opts.verbose),
8347
8936
  fix: Boolean(opts.fix)
8348
8937
  });
8349
- const checkPort = await this.checkPortAvailability({
8350
- host: report.process.running ? report.endpoints.uiUrl ? new URL(report.endpoints.uiUrl).hostname : "127.0.0.1" : "127.0.0.1",
8351
- port: (() => {
8352
- try {
8353
- const base = report.process.running && report.endpoints.uiUrl ? report.endpoints.uiUrl : report.endpoints.configuredUiUrl;
8354
- return Number(new URL(base).port || 80);
8355
- } catch {
8356
- return 55667;
8357
- }
8358
- })()
8938
+ const checkPort = await this.checkPortAvailability(this.resolveDoctorPortCheckTarget(report));
8939
+ const checks = this.buildDoctorChecks(report, checkPort);
8940
+ const exitCode = this.resolveDoctorExitCode(checks);
8941
+ if (opts.json) {
8942
+ console.log(JSON.stringify({
8943
+ generatedAt: report.generatedAt,
8944
+ checks,
8945
+ status: report,
8946
+ exitCode
8947
+ }, null, 2));
8948
+ process.exitCode = exitCode;
8949
+ return;
8950
+ }
8951
+ printDoctorReport({
8952
+ logo: this.deps.logo,
8953
+ generatedAt: report.generatedAt,
8954
+ checks,
8955
+ recommendations: report.recommendations,
8956
+ verbose: Boolean(opts.verbose),
8957
+ logTail: report.logTail
8359
8958
  });
8959
+ process.exitCode = exitCode;
8960
+ };
8961
+ resolveDoctorPortCheckTarget = (report) => {
8962
+ const host = report.process.running && report.endpoints.uiUrl ? new URL(report.endpoints.uiUrl).hostname : "127.0.0.1";
8963
+ try {
8964
+ const base = report.process.running && report.endpoints.uiUrl ? report.endpoints.uiUrl : report.endpoints.configuredUiUrl;
8965
+ return {
8966
+ host,
8967
+ port: Number(new URL(base).port || 80)
8968
+ };
8969
+ } catch {
8970
+ return {
8971
+ host,
8972
+ port: 55667
8973
+ };
8974
+ }
8975
+ };
8976
+ buildDoctorChecks = (report, checkPort) => {
8360
8977
  const providerConfigured = report.providers.some((provider) => provider.configured);
8361
- const checks = [
8978
+ return [
8362
8979
  {
8363
8980
  name: "config-file",
8364
8981
  status: report.configExists ? "pass" : "fail",
@@ -8376,12 +8993,12 @@ var DiagnosticsCommands = class {
8376
8993
  },
8377
8994
  {
8378
8995
  name: "service-health",
8379
- status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : report.health.configured.state === "ok" ? "warn" : "warn",
8996
+ status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : "warn",
8380
8997
  detail: report.process.running ? `${report.health.managed.state}: ${report.health.managed.detail}` : `${report.health.configured.state}: ${report.health.configured.detail}`
8381
8998
  },
8382
8999
  {
8383
9000
  name: "ui-port-availability",
8384
- status: report.process.running ? "pass" : checkPort.available ? "pass" : "fail",
9001
+ status: report.process.running || checkPort.available ? "pass" : "fail",
8385
9002
  detail: report.process.running ? "managed by running service" : checkPort.available ? "available" : checkPort.detail
8386
9003
  },
8387
9004
  {
@@ -8390,40 +9007,23 @@ var DiagnosticsCommands = class {
8390
9007
  detail: providerConfigured ? "at least one provider configured" : "no provider api key configured"
8391
9008
  }
8392
9009
  ];
8393
- const failed = checks.filter((check) => check.status === "fail");
8394
- const warned = checks.filter((check) => check.status === "warn");
8395
- const exitCode = failed.length > 0 ? 1 : warned.length > 0 ? 1 : 0;
8396
- if (opts.json) {
8397
- console.log(JSON.stringify({
8398
- generatedAt: report.generatedAt,
8399
- checks,
8400
- status: report,
8401
- exitCode
8402
- }, null, 2));
8403
- process.exitCode = exitCode;
8404
- return;
8405
- }
8406
- printDoctorReport({
8407
- logo: this.deps.logo,
8408
- generatedAt: report.generatedAt,
8409
- checks,
8410
- recommendations: report.recommendations,
8411
- verbose: Boolean(opts.verbose),
8412
- logTail: report.logTail
8413
- });
8414
- process.exitCode = exitCode;
8415
- }
8416
- async collectRuntimeStatus(params) {
9010
+ };
9011
+ resolveDoctorExitCode = (checks) => {
9012
+ if (checks.some((check) => check.status === "fail")) return 1;
9013
+ if (checks.some((check) => check.status === "warn")) return 1;
9014
+ return 0;
9015
+ };
9016
+ collectRuntimeStatus = async (params) => {
8417
9017
  const configPath = getConfigPath();
8418
9018
  const config = loadConfig();
8419
9019
  const workspacePath = getWorkspacePath(config.agents.defaults.workspace);
8420
- const serviceStatePath = resolve(getDataDir(), "run", "service.json");
9020
+ const serviceStatePath = managedServiceStateStore.path;
8421
9021
  const fixActions = [];
8422
- let serviceState = readServiceState();
9022
+ let serviceState = managedServiceStateStore.read();
8423
9023
  if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
8424
- clearServiceState();
9024
+ managedServiceStateStore.clear();
8425
9025
  fixActions.push("Cleared stale service state file.");
8426
- serviceState = readServiceState();
9026
+ serviceState = managedServiceStateStore.read();
8427
9027
  }
8428
9028
  const managedByState = Boolean(serviceState);
8429
9029
  const running = Boolean(serviceState && isProcessRunning(serviceState.pid));
@@ -8459,7 +9059,7 @@ var DiagnosticsCommands = class {
8459
9059
  issues,
8460
9060
  recommendations
8461
9061
  });
8462
- const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveServiceLogPath(), 25) : [];
9062
+ const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveAppLogPath("service"), 25) : [];
8463
9063
  const level = running ? managedHealth.state === "ok" ? issues.length > 0 ? "degraded" : "healthy" : "degraded" : "stopped";
8464
9064
  return {
8465
9065
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8497,8 +9097,8 @@ var DiagnosticsCommands = class {
8497
9097
  level,
8498
9098
  exitCode: 0
8499
9099
  };
8500
- }
8501
- async probeApiHealth(url, timeoutMs = 1500) {
9100
+ };
9101
+ probeApiHealth = async (url, timeoutMs = 1500) => {
8502
9102
  const controller = new AbortController();
8503
9103
  const timer = setTimeout(() => controller.abort(), timeoutMs);
8504
9104
  try {
@@ -8529,8 +9129,8 @@ var DiagnosticsCommands = class {
8529
9129
  } finally {
8530
9130
  clearTimeout(timer);
8531
9131
  }
8532
- }
8533
- listProviderStatuses(config) {
9132
+ };
9133
+ listProviderStatuses = (config) => {
8534
9134
  return listBuiltinProviders().map((spec) => {
8535
9135
  const provider = config.providers[spec.name];
8536
9136
  const apiKeyRefSet = hasSecretRef(config, `providers.${spec.name}.apiKey`);
@@ -8555,8 +9155,8 @@ var DiagnosticsCommands = class {
8555
9155
  detail: provider.apiKey ? "apiKey set" : apiKeyRefSet ? "apiKey ref set" : "apiKey not set"
8556
9156
  };
8557
9157
  });
8558
- }
8559
- collectRuntimeIssues(params) {
9158
+ };
9159
+ collectRuntimeIssues = (params) => {
8560
9160
  if (!existsSync(params.configPath)) {
8561
9161
  params.issues.push("Config file is missing.");
8562
9162
  params.recommendations.push(`Run ${APP_NAME} init to create config files.`);
@@ -8571,7 +9171,7 @@ var DiagnosticsCommands = class {
8571
9171
  }
8572
9172
  if (params.running && params.managedHealth.state !== "ok") {
8573
9173
  params.issues.push(`Managed service health check failed: ${params.managedHealth.detail}`);
8574
- params.recommendations.push(`Check logs at ${params.serviceState?.logPath ?? resolveServiceLogPath()}.`);
9174
+ params.recommendations.push(`Check logs at ${params.serviceState?.logPath ?? resolveAppLogPath("service")}.`);
8575
9175
  }
8576
9176
  if (params.running && params.serviceState?.startupState === "degraded" && params.managedHealth.state !== "ok") {
8577
9177
  const startupHint = params.serviceState.startupLastProbeError ? ` (${params.serviceState.startupLastProbeError})` : "";
@@ -8584,8 +9184,8 @@ var DiagnosticsCommands = class {
8584
9184
  params.recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
8585
9185
  }
8586
9186
  if (!params.providers.some((provider) => provider.configured)) params.recommendations.push("Configure at least one provider API key in UI or config before expecting agent replies.");
8587
- }
8588
- readLogTail(path, maxLines = 25) {
9187
+ };
9188
+ readLogTail = (path, maxLines = 25) => {
8589
9189
  if (!existsSync(path)) return [];
8590
9190
  try {
8591
9191
  const lines = readFileSync(path, "utf-8").split(/\r?\n/).filter(Boolean);
@@ -8594,8 +9194,8 @@ var DiagnosticsCommands = class {
8594
9194
  } catch {
8595
9195
  return [];
8596
9196
  }
8597
- }
8598
- async checkPortAvailability(params) {
9197
+ };
9198
+ checkPortAvailability = async (params) => {
8599
9199
  return await new Promise((resolve) => {
8600
9200
  const server = createServer();
8601
9201
  server.once("error", (error) => {
@@ -8613,7 +9213,34 @@ var DiagnosticsCommands = class {
8613
9213
  });
8614
9214
  });
8615
9215
  });
8616
- }
9216
+ };
9217
+ };
9218
+ //#endregion
9219
+ //#region src/cli/commands/logs.ts
9220
+ var LogsCommands = class {
9221
+ constructor(runtime = getLoggingRuntime()) {
9222
+ this.runtime = runtime;
9223
+ }
9224
+ logsPath = () => {
9225
+ const paths = this.runtime.getPaths();
9226
+ console.log([
9227
+ `Logs directory: ${paths.logsDir}`,
9228
+ `Service log: ${paths.serviceLogPath}`,
9229
+ `Crash log: ${paths.crashLogPath}`,
9230
+ `Archive: ${paths.archiveDir}`
9231
+ ].join("\n"));
9232
+ };
9233
+ logsTail = (opts = {}) => {
9234
+ const kind = opts.crash ? "crash" : "service";
9235
+ const rawLines = Number(opts.lines);
9236
+ const lines = Number.isFinite(rawLines) && rawLines > 0 ? Math.floor(rawLines) : 40;
9237
+ const output = this.runtime.tail(kind, lines);
9238
+ if (output.length === 0) {
9239
+ console.log(`No log entries found in ${this.runtime.resolveLogPath(kind)}.`);
9240
+ return;
9241
+ }
9242
+ console.log(output.join("\n"));
9243
+ };
8617
9244
  };
8618
9245
  //#endregion
8619
9246
  //#region src/cli/commands/service-support/runtime/service-port-probe.ts
@@ -8732,17 +9359,42 @@ async function probeHealthEndpoint(healthUrl) {
8732
9359
  req.end();
8733
9360
  });
8734
9361
  }
9362
+ async function inspectUiTarget(params) {
9363
+ const { checkPortAvailabilityFn, healthUrl, host, port, probeHealthEndpointFn } = params;
9364
+ const availability = await (checkPortAvailabilityFn ?? checkPortAvailability)({
9365
+ host,
9366
+ port
9367
+ });
9368
+ if (availability.available) return {
9369
+ state: "available",
9370
+ availabilityDetail: availability.detail,
9371
+ probeError: null
9372
+ };
9373
+ const probe = await (probeHealthEndpointFn ?? probeHealthEndpoint)(healthUrl);
9374
+ if (probe.healthy) return {
9375
+ state: "healthy-existing",
9376
+ availabilityDetail: availability.detail,
9377
+ probeError: null
9378
+ };
9379
+ return {
9380
+ state: "occupied-unhealthy",
9381
+ availabilityDetail: availability.detail,
9382
+ probeError: probe.error
9383
+ };
9384
+ }
8735
9385
  async function describeUnmanagedHealthyTargetMessage(params) {
8736
9386
  const uiConfig = resolveUiConfig(loadConfig(), params.uiOverrides);
8737
9387
  const healthUrl = `${resolveUiApiBase(uiConfig.host, uiConfig.port)}/api/health`;
8738
- if ((await (params.checkPortAvailabilityFn ?? checkPortAvailability)({
9388
+ if ((await inspectUiTarget({
8739
9389
  host: uiConfig.host,
8740
- port: uiConfig.port
8741
- })).available) return null;
8742
- if (!(await (params.probeHealthEndpointFn ?? probeHealthEndpoint)(healthUrl)).healthy) return null;
9390
+ port: uiConfig.port,
9391
+ healthUrl,
9392
+ checkPortAvailabilityFn: params.checkPortAvailabilityFn,
9393
+ probeHealthEndpointFn: params.probeHealthEndpointFn
9394
+ })).state !== "healthy-existing") return null;
8743
9395
  return [
8744
9396
  `Target UI health: ${healthUrl}`,
8745
- `A healthy ${APP_NAME} service is already responding on this port, but it is not tracked by ${resolveServiceStatePath()}.`,
9397
+ `A healthy ${APP_NAME} service is already responding on this port, but it is not tracked by ${managedServiceStateStore.path}.`,
8746
9398
  `${APP_NAME} restart only stops the background service recorded in managed state; it will not auto-kill Docker or other external listeners.`,
8747
9399
  `Fix: stop that external service first or rerun with --ui-port <port>.`
8748
9400
  ].join("\n");
@@ -9076,7 +9728,7 @@ function claimManagedRemoteRuntimeOwnership(params) {
9076
9728
  const managedConflict = detectManagedRemoteOwnershipConflict({
9077
9729
  currentPid,
9078
9730
  isProcessRunningFn,
9079
- readServiceStateFn: params.readServiceStateFn ?? readServiceState
9731
+ readServiceStateFn: params.readServiceStateFn ?? managedServiceStateStore.read
9080
9732
  });
9081
9733
  if (managedConflict) return {
9082
9734
  ok: false,
@@ -9119,7 +9771,7 @@ function createManagedRemoteModuleForUi(params) {
9119
9771
  });
9120
9772
  }
9121
9773
  function writeInitialManagedServiceState(params) {
9122
- writeServiceState({
9774
+ managedServiceStateStore.write({
9123
9775
  pid: params.snapshot.pid,
9124
9776
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9125
9777
  uiUrl: params.snapshot.uiUrl,
@@ -9134,7 +9786,7 @@ function writeInitialManagedServiceState(params) {
9134
9786
  });
9135
9787
  }
9136
9788
  function writeReadyManagedServiceState(params) {
9137
- const currentState = readServiceState();
9789
+ const currentState = managedServiceStateStore.read();
9138
9790
  const state = {
9139
9791
  pid: params.snapshot.pid,
9140
9792
  startedAt: currentState?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -9149,7 +9801,7 @@ function writeReadyManagedServiceState(params) {
9149
9801
  startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
9150
9802
  ...currentState?.remote ? { remote: currentState.remote } : {}
9151
9803
  };
9152
- writeServiceState(state);
9804
+ managedServiceStateStore.write(state);
9153
9805
  return state;
9154
9806
  }
9155
9807
  //#endregion
@@ -9197,8 +9849,8 @@ function resolveSessionRouteCandidate(params) {
9197
9849
  function spawnManagedService(params) {
9198
9850
  const { appName, config, uiConfig, uiUrl, apiUrl, healthUrl, startupTimeoutMs, resolveStartupTimeoutMs, appendStartupStage, printStartupFailureDiagnostics, resolveServiceLogPath } = params;
9199
9851
  const logPath = resolveServiceLogPath();
9200
- mkdirSync(resolve(logPath, ".."), { recursive: true });
9201
- const logFd = openSync(logPath, "a");
9852
+ new FileLogSink({ serviceLogPath: logPath }).ensureReady();
9853
+ mkdirSync(dirname(logPath), { recursive: true });
9202
9854
  const readinessTimeoutMs = resolveStartupTimeoutMs(startupTimeoutMs);
9203
9855
  const quickPhaseTimeoutMs = Math.min(8e3, readinessTimeoutMs);
9204
9856
  const extendedPhaseTimeoutMs = Math.max(0, readinessTimeoutMs - quickPhaseTimeoutMs);
@@ -9218,15 +9870,10 @@ function spawnManagedService(params) {
9218
9870
  appendStartupStage(logPath, `spawning background process: ${cliLaunch.command} ${childArgs.join(" ")}`);
9219
9871
  const child = spawn(cliLaunch.command, childArgs, {
9220
9872
  env: process.env,
9221
- stdio: [
9222
- "ignore",
9223
- logFd,
9224
- logFd
9225
- ],
9873
+ stdio: "ignore",
9226
9874
  detached: true
9227
9875
  });
9228
9876
  appendStartupStage(logPath, `spawned background process pid=${child.pid ?? "unknown"}`);
9229
- closeSync(logFd);
9230
9877
  if (!child.pid) {
9231
9878
  appendStartupStage(logPath, "spawn failed: child pid missing");
9232
9879
  console.error("Error: Failed to start background service.");
@@ -9371,32 +10018,10 @@ var ServiceFileWatcherRegistry = class {
9371
10018
  }));
9372
10019
  };
9373
10020
  };
9374
- function writeLocalServiceDiscoveryState(uiConfig, pid = process.pid) {
9375
- const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
9376
- const apiUrl = `${uiUrl}/api`;
9377
- const existing = readServiceState();
9378
- writeServiceState({
9379
- pid,
9380
- startedAt: existing?.pid === pid && typeof existing.startedAt === "string" ? existing.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
9381
- uiUrl,
9382
- apiUrl,
9383
- uiHost: uiConfig.host,
9384
- uiPort: uiConfig.port,
9385
- logPath: existing?.logPath ?? resolveServiceLogPath(),
9386
- startupState: existing?.startupState ?? "ready",
9387
- startupLastProbeError: existing?.startupLastProbeError ?? null,
9388
- startupTimeoutMs: existing?.startupTimeoutMs,
9389
- startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
9390
- ...existing?.remote ? { remote: existing.remote } : {}
9391
- });
9392
- }
9393
- function clearOwnedServiceState(pid = process.pid) {
9394
- if (readServiceState()?.pid === pid) clearServiceState();
9395
- }
9396
10021
  function finalizeLocalUiStartup(params) {
9397
10022
  const { setUiEventPublisher, uiConfig, uiStartup } = params;
9398
10023
  setUiEventPublisher(uiStartup?.publish);
9399
- if (uiStartup) writeLocalServiceDiscoveryState(uiConfig);
10024
+ if (uiStartup) localUiRuntimeStore.writeCurrentProcess(uiConfig);
9400
10025
  }
9401
10026
  async function startGatewayRuntimeSupport(params) {
9402
10027
  const { cronJobs, cronStorePath, reloadCronStore, remoteModule, startCron, startHeartbeat, watchConfigFile, watcherRegistry } = params;
@@ -9423,7 +10048,7 @@ function resolveRemoteServiceView(currentUi) {
9423
10048
  uiUrl: resolveUiApiBase(currentUi.host, currentUi.port),
9424
10049
  uiPort: currentUi.port
9425
10050
  };
9426
- const serviceState = readServiceState();
10051
+ const serviceState = managedServiceStateStore.read();
9427
10052
  const serviceRunning = Boolean(serviceState && isProcessRunning(serviceState.pid));
9428
10053
  return {
9429
10054
  running: serviceRunning,
@@ -9462,7 +10087,7 @@ async function controlCurrentProcessRuntime(action, controller) {
9462
10087
  };
9463
10088
  }
9464
10089
  async function controlManagedService(action, deps) {
9465
- const state = readServiceState();
10090
+ const state = managedServiceStateStore.read();
9466
10091
  const running = Boolean(state && isProcessRunning(state.pid));
9467
10092
  const currentProcess = Boolean(running && state?.pid === process.pid);
9468
10093
  const uiOverrides = resolveManagedUiOverrides();
@@ -9548,7 +10173,7 @@ function launchManagedSelfControl(params = {}) {
9548
10173
  "const { spawn } = require(\"node:child_process\");",
9549
10174
  "const { rmSync } = require(\"node:fs\");",
9550
10175
  `const parentPid = ${process.pid};`,
9551
- `const serviceStatePath = ${JSON.stringify(resolveServiceStatePath())};`,
10176
+ `const serviceStatePath = ${JSON.stringify(managedServiceStateStore.path)};`,
9552
10177
  `const command = ${JSON.stringify(params.command ?? null)};`,
9553
10178
  `const args = ${JSON.stringify(params.args ?? [])};`,
9554
10179
  `const cwd = ${JSON.stringify(process.cwd())};`,
@@ -9597,7 +10222,7 @@ function launchManagedSelfControl(params = {}) {
9597
10222
  }
9598
10223
  //#endregion
9599
10224
  //#region src/cli/commands/remote-support/remote-access-host.ts
9600
- function normalizeOptionalString$2(value) {
10225
+ function normalizeOptionalString$4(value) {
9601
10226
  if (typeof value !== "string") return null;
9602
10227
  const trimmed = value.trim();
9603
10228
  return trimmed.length > 0 ? trimmed : null;
@@ -9626,8 +10251,8 @@ var RemoteAccessHost = class {
9626
10251
  const status = this.deps.remoteCommands.getStatusView();
9627
10252
  return {
9628
10253
  account: this.readAccountView({
9629
- token: normalizeOptionalString$2(config.providers.nextclaw?.apiKey),
9630
- apiBase: normalizeOptionalString$2(config.providers.nextclaw?.apiBase),
10254
+ token: normalizeOptionalString$4(config.providers.nextclaw?.apiKey),
10255
+ apiBase: normalizeOptionalString$4(config.providers.nextclaw?.apiBase),
9631
10256
  platformBase: status.platformBase
9632
10257
  }),
9633
10258
  settings: {
@@ -9658,7 +10283,7 @@ var RemoteAccessHost = class {
9658
10283
  pollBrowserAuth = async (input) => {
9659
10284
  const config = loadConfig(getConfigPath());
9660
10285
  const result = await this.deps.platformAuthCommands.pollBrowserAuth({
9661
- apiBase: normalizeOptionalString$2(input.apiBase) ?? normalizeOptionalString$2(config.remote.platformApiBase) ?? normalizeOptionalString$2(config.providers.nextclaw?.apiBase) ?? void 0,
10286
+ apiBase: normalizeOptionalString$4(input.apiBase) ?? normalizeOptionalString$4(config.remote.platformApiBase) ?? normalizeOptionalString$4(config.providers.nextclaw?.apiBase) ?? void 0,
9662
10287
  sessionId: input.sessionId
9663
10288
  });
9664
10289
  if (result.status !== "authorized") return result;
@@ -9784,51 +10409,71 @@ var AssetPutTool = class {
9784
10409
  description = "Put a normal file path or base64 bytes into the managed asset store.";
9785
10410
  parameters = {
9786
10411
  type: "object",
9787
- properties: {
9788
- path: {
9789
- type: "string",
9790
- description: "Existing local file path to put into the asset store."
9791
- },
9792
- bytesBase64: {
9793
- type: "string",
9794
- description: "Base64 file bytes. Use together with fileName when no source path exists."
10412
+ oneOf: [{
10413
+ type: "object",
10414
+ properties: {
10415
+ path: {
10416
+ type: "string",
10417
+ description: "Existing local file path to put into the asset store."
10418
+ },
10419
+ fileName: {
10420
+ type: "string",
10421
+ description: "Optional asset file name override."
10422
+ },
10423
+ mimeType: {
10424
+ type: "string",
10425
+ description: "Optional mime type override."
10426
+ }
9795
10427
  },
9796
- fileName: {
9797
- type: "string",
9798
- description: "Optional asset file name override. Required when using bytesBase64."
10428
+ required: ["path"],
10429
+ additionalProperties: false
10430
+ }, {
10431
+ type: "object",
10432
+ properties: {
10433
+ bytesBase64: {
10434
+ type: "string",
10435
+ description: "Base64 file bytes. Use together with fileName when no source path exists."
10436
+ },
10437
+ fileName: {
10438
+ type: "string",
10439
+ description: "Asset file name. Required when using bytesBase64."
10440
+ },
10441
+ mimeType: {
10442
+ type: "string",
10443
+ description: "Optional mime type override."
10444
+ }
9799
10445
  },
9800
- mimeType: {
9801
- type: "string",
9802
- description: "Optional mime type override."
9803
- }
9804
- }
10446
+ required: ["bytesBase64", "fileName"],
10447
+ additionalProperties: false
10448
+ }]
9805
10449
  };
9806
10450
  constructor(assetStore, contentBasePath) {
9807
10451
  this.assetStore = assetStore;
9808
10452
  this.contentBasePath = contentBasePath;
9809
10453
  }
9810
- async execute(args) {
10454
+ execute = async (args) => {
9811
10455
  const path = readOptionalString$5(args?.path);
9812
10456
  const fileName = readOptionalString$5(args?.fileName);
9813
10457
  const mimeType = readOptionalString$5(args?.mimeType);
9814
10458
  const bytes = readOptionalBase64Bytes(args?.bytesBase64);
9815
- let record;
9816
- if (path) record = await this.assetStore.putPath({
9817
- path,
9818
- fileName: fileName ?? void 0,
9819
- mimeType
9820
- });
9821
- else if (bytes && fileName) record = await this.assetStore.putBytes({
9822
- fileName,
9823
- mimeType,
9824
- bytes
9825
- });
9826
- else throw new Error("asset_put requires either path, or bytesBase64 + fileName.");
9827
- return {
10459
+ if (path) return {
9828
10460
  ok: true,
9829
- asset: toAssetPayload(record, this.contentBasePath)
10461
+ asset: toAssetPayload(await this.assetStore.putPath({
10462
+ path,
10463
+ fileName: fileName ?? void 0,
10464
+ mimeType
10465
+ }), this.contentBasePath)
9830
10466
  };
9831
- }
10467
+ if (bytes && fileName) return {
10468
+ ok: true,
10469
+ asset: toAssetPayload(await this.assetStore.putBytes({
10470
+ fileName,
10471
+ mimeType,
10472
+ bytes
10473
+ }), this.contentBasePath)
10474
+ };
10475
+ throw new Error("asset_put received invalid arguments after validation.");
10476
+ };
9832
10477
  };
9833
10478
  var AssetExportTool = class {
9834
10479
  name = "asset_export";
@@ -9845,12 +10490,13 @@ var AssetExportTool = class {
9845
10490
  description: "Destination file path."
9846
10491
  }
9847
10492
  },
9848
- required: ["assetUri", "targetPath"]
10493
+ required: ["assetUri", "targetPath"],
10494
+ additionalProperties: false
9849
10495
  };
9850
10496
  constructor(assetStore) {
9851
10497
  this.assetStore = assetStore;
9852
10498
  }
9853
- async execute(args) {
10499
+ execute = async (args) => {
9854
10500
  const assetUri = readOptionalString$5(args?.assetUri);
9855
10501
  const targetPath = readOptionalString$5(args?.targetPath);
9856
10502
  if (!assetUri || !targetPath) throw new Error("asset_export requires assetUri and targetPath.");
@@ -9859,7 +10505,7 @@ var AssetExportTool = class {
9859
10505
  assetUri,
9860
10506
  exportedPath: await this.assetStore.export({ uri: assetUri }, resolve(targetPath))
9861
10507
  };
9862
- }
10508
+ };
9863
10509
  };
9864
10510
  var AssetStatTool = class {
9865
10511
  name = "asset_stat";
@@ -9870,13 +10516,14 @@ var AssetStatTool = class {
9870
10516
  type: "string",
9871
10517
  description: "Managed asset URI to inspect."
9872
10518
  } },
9873
- required: ["assetUri"]
10519
+ required: ["assetUri"],
10520
+ additionalProperties: false
9874
10521
  };
9875
10522
  constructor(assetStore, contentBasePath) {
9876
10523
  this.assetStore = assetStore;
9877
10524
  this.contentBasePath = contentBasePath;
9878
10525
  }
9879
- async execute(args) {
10526
+ execute = async (args) => {
9880
10527
  const assetUri = readOptionalString$5(args?.assetUri);
9881
10528
  if (!assetUri) throw new Error("asset_stat requires assetUri.");
9882
10529
  const record = await this.assetStore.statRecord(assetUri);
@@ -9891,7 +10538,7 @@ var AssetStatTool = class {
9891
10538
  ok: true,
9892
10539
  asset: toAssetPayload(record, this.contentBasePath)
9893
10540
  };
9894
- }
10541
+ };
9895
10542
  };
9896
10543
  function createAssetTools(params) {
9897
10544
  const contentBasePath = params.contentBasePath ?? "/api/ncp/assets/content";
@@ -10042,7 +10689,7 @@ function toLegacyMessages(messages, options = {}) {
10042
10689
  }
10043
10690
  //#endregion
10044
10691
  //#region src/cli/commands/ncp/context/nextclaw-ncp-session-preferences.ts
10045
- function normalizeOptionalString$1(value) {
10692
+ function normalizeOptionalString$3(value) {
10046
10693
  if (typeof value !== "string") return;
10047
10694
  const trimmed = value.trim();
10048
10695
  return trimmed.length > 0 ? trimmed : void 0;
@@ -10055,7 +10702,7 @@ function readMetadataModel(metadata) {
10055
10702
  metadata.session_model
10056
10703
  ];
10057
10704
  for (const candidate of candidates) {
10058
- const normalized = normalizeOptionalString$1(candidate);
10705
+ const normalized = normalizeOptionalString$3(candidate);
10059
10706
  if (normalized) return normalized;
10060
10707
  }
10061
10708
  return null;
@@ -10082,7 +10729,7 @@ function resolveEffectiveModel(params) {
10082
10729
  if (params.requestMetadata.clear_model === true || params.requestMetadata.reset_model === true) delete params.session.metadata.preferred_model;
10083
10730
  const inboundModel = readMetadataModel(params.requestMetadata);
10084
10731
  if (inboundModel) params.session.metadata.preferred_model = inboundModel;
10085
- return normalizeOptionalString$1(params.session.metadata.preferred_model) ?? params.fallbackModel;
10732
+ return normalizeOptionalString$3(params.session.metadata.preferred_model) ?? params.fallbackModel;
10086
10733
  }
10087
10734
  function syncSessionThinkingPreference(params) {
10088
10735
  if (params.requestMetadata.clear_thinking === true || params.requestMetadata.reset_thinking === true) delete params.session.metadata.preferred_thinking;
@@ -10094,8 +10741,8 @@ function syncSessionThinkingPreference(params) {
10094
10741
  if (inboundThinking) params.session.metadata.preferred_thinking = inboundThinking;
10095
10742
  }
10096
10743
  function resolveSessionChannelContext(params) {
10097
- const channel = normalizeOptionalString$1(params.requestMetadata.channel) ?? normalizeOptionalString$1(params.session.metadata.last_channel) ?? "ui";
10098
- const chatId = normalizeOptionalString$1(params.requestMetadata.chatId) ?? normalizeOptionalString$1(params.requestMetadata.chat_id) ?? normalizeOptionalString$1(params.session.metadata.last_to) ?? "web-ui";
10744
+ const channel = normalizeOptionalString$3(params.requestMetadata.channel) ?? normalizeOptionalString$3(params.session.metadata.last_channel) ?? "ui";
10745
+ const chatId = normalizeOptionalString$3(params.requestMetadata.chatId) ?? normalizeOptionalString$3(params.requestMetadata.chat_id) ?? normalizeOptionalString$3(params.session.metadata.last_to) ?? "web-ui";
10099
10746
  params.session.metadata.last_channel = channel;
10100
10747
  params.session.metadata.last_to = chatId;
10101
10748
  return {
@@ -10739,7 +11386,7 @@ var NextclawNcpContextBuilder = class {
10739
11386
  accountId: accountId ?? null
10740
11387
  });
10741
11388
  const toolDefinitions = this.options.toolRegistry.getToolDefinitions();
10742
- const additionalSystemSections = [buildSessionOrchestrationSection()];
11389
+ const additionalSystemSections = [buildSessionOrchestrationSection(), buildMinimalSystemExecutionPrompt(effectiveModel)].filter(Boolean);
10743
11390
  const contextBuilder = new ContextBuilder(effectiveWorkspace, config.agents.context, {
10744
11391
  hostWorkspace: profile.workspace,
10745
11392
  sessionProjectRoot: readSessionProjectRoot(session.metadata)
@@ -11302,12 +11949,6 @@ function extractSessionMessageText(message) {
11302
11949
  if (parts.length === 0) return;
11303
11950
  return parts.join("\n\n");
11304
11951
  }
11305
- function findLatestAssistantMessage(messages) {
11306
- for (let index = messages.length - 1; index >= 0; index -= 1) {
11307
- const message = messages[index];
11308
- if (message?.role === "assistant") return message;
11309
- }
11310
- }
11311
11952
  function readParentSessionId(metadata) {
11312
11953
  return readOptionalString$1(metadata?.["parent_session_id"]) ?? void 0;
11313
11954
  }
@@ -11527,8 +12168,6 @@ var SessionRequestBroker = class {
11527
12168
  task
11528
12169
  });
11529
12170
  if (streamedMessage) return streamedMessage;
11530
- const fallbackMessage = findLatestAssistantMessage(await backend.listSessionMessages(request.targetSessionId));
11531
- if (fallbackMessage) return fallbackMessage;
11532
12171
  throw new Error("Session request completed without a final reply.");
11533
12172
  };
11534
12173
  appendRequestEvents = (request, type) => {
@@ -11710,33 +12349,151 @@ var SessionRequestDeliveryService = class {
11710
12349
  };
11711
12350
  };
11712
12351
  //#endregion
12352
+ //#region src/cli/commands/shared/llm-usage-observer.ts
12353
+ var LlmUsageObserver = class {
12354
+ constructor(recorder, source) {
12355
+ this.recorder = recorder;
12356
+ this.source = source;
12357
+ }
12358
+ observe = (params) => {
12359
+ return this.recorder.record({
12360
+ source: this.source,
12361
+ model: params.model ?? null,
12362
+ usage: params.usage
12363
+ });
12364
+ };
12365
+ };
12366
+ var ObservedProviderManager = class extends ProviderManager {
12367
+ constructor(delegate, observer) {
12368
+ super(delegate.get(null));
12369
+ this.delegate = delegate;
12370
+ this.observer = observer;
12371
+ }
12372
+ get(model) {
12373
+ return this.delegate.get(model);
12374
+ }
12375
+ set(next) {
12376
+ this.delegate.set(next);
12377
+ }
12378
+ setConfig(nextConfig) {
12379
+ this.delegate.setConfig(nextConfig);
12380
+ }
12381
+ async chat(params) {
12382
+ const response = await this.delegate.chat(params);
12383
+ this.observer.observe({
12384
+ model: params.model ?? null,
12385
+ usage: response.usage
12386
+ });
12387
+ return response;
12388
+ }
12389
+ async *chatStream(params) {
12390
+ for await (const event of this.delegate.chatStream(params)) {
12391
+ if (event.type === "done") this.observer.observe({
12392
+ model: params.model ?? null,
12393
+ usage: event.response.usage
12394
+ });
12395
+ yield event;
12396
+ }
12397
+ }
12398
+ };
12399
+ //#endregion
11713
12400
  //#region src/cli/commands/ncp/runtime/ui-ncp-agent-handle.ts
11714
12401
  function createUiNcpAgentHandle(params) {
12402
+ const { backend, runtimeRegistry, refreshPluginRuntimeRegistrations, applyExtensionRegistry, applyMcpConfig, dispose, assetStore } = params;
11715
12403
  return {
11716
12404
  basePath: "/api/ncp/agent",
11717
- agentClientEndpoint: createAgentClientFromServer(params.backend),
11718
- streamProvider: params.backend,
11719
- sessionApi: params.backend,
12405
+ agentClientEndpoint: createAgentClientFromServer(backend),
12406
+ streamProvider: backend,
12407
+ runApi: backend,
12408
+ sessionApi: backend,
11720
12409
  listSessionTypes: (describeParams) => {
11721
- params.refreshPluginRuntimeRegistrations();
11722
- return params.runtimeRegistry.listSessionTypes(describeParams);
12410
+ refreshPluginRuntimeRegistrations();
12411
+ return runtimeRegistry.listSessionTypes(describeParams);
11723
12412
  },
11724
12413
  assetApi: {
11725
- put: (input) => params.assetStore.putBytes({
12414
+ put: (input) => assetStore.putBytes({
11726
12415
  fileName: input.fileName,
11727
12416
  mimeType: input.mimeType,
11728
12417
  bytes: input.bytes,
11729
12418
  createdAt: input.createdAt
11730
12419
  }),
11731
- stat: (uri) => params.assetStore.statRecord(uri),
11732
- resolveContentPath: (uri) => params.assetStore.resolveContentPath(uri)
12420
+ stat: (uri) => assetStore.statRecord(uri),
12421
+ resolveContentPath: (uri) => assetStore.resolveContentPath(uri)
11733
12422
  },
11734
- applyExtensionRegistry: params.applyExtensionRegistry,
11735
- applyMcpConfig: params.applyMcpConfig,
11736
- dispose: params.dispose
12423
+ applyExtensionRegistry,
12424
+ applyMcpConfig,
12425
+ dispose
11737
12426
  };
11738
12427
  }
11739
12428
  //#endregion
12429
+ //#region src/cli/runtime-state/llm-usage-record.ts
12430
+ var LlmUsageRecordFactory = class {
12431
+ create = (params) => {
12432
+ const { observedAt, source, model, usage: rawUsage } = params;
12433
+ const usage = this.sanitizeUsage(rawUsage);
12434
+ return {
12435
+ version: 1,
12436
+ observedAt: observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
12437
+ source,
12438
+ model: this.normalizeModel(model),
12439
+ usage,
12440
+ summary: this.buildSummary(usage)
12441
+ };
12442
+ };
12443
+ sanitizeUsage = (usage) => {
12444
+ const next = {};
12445
+ for (const [key, value] of Object.entries(usage)) {
12446
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) continue;
12447
+ next[key] = Math.floor(value);
12448
+ }
12449
+ return next;
12450
+ };
12451
+ buildSummary = (usage) => {
12452
+ const promptTokens = usage.prompt_tokens ?? usage.input_tokens ?? 0;
12453
+ const completionTokens = usage.completion_tokens ?? usage.output_tokens ?? 0;
12454
+ const totalTokens = usage.total_tokens ?? promptTokens + completionTokens;
12455
+ const cacheMetricKeys = Object.keys(usage).filter((key) => key.endsWith("cached_tokens"));
12456
+ const cachedTokens = cacheMetricKeys.reduce((max, key) => Math.max(max, usage[key] ?? 0), 0);
12457
+ return {
12458
+ promptTokens,
12459
+ completionTokens,
12460
+ totalTokens,
12461
+ cachedTokens,
12462
+ cacheHit: cachedTokens > 0,
12463
+ cacheMetricKeys
12464
+ };
12465
+ };
12466
+ normalizeModel = (value) => {
12467
+ if (typeof value !== "string") return null;
12468
+ const trimmed = value.trim();
12469
+ return trimmed.length > 0 ? trimmed : null;
12470
+ };
12471
+ };
12472
+ const llmUsageRecordFactory = new LlmUsageRecordFactory();
12473
+ //#endregion
12474
+ //#region src/cli/commands/shared/llm-usage-recorder.ts
12475
+ var LlmUsageRecorder = class {
12476
+ constructor(deps = {}) {
12477
+ this.deps = deps;
12478
+ }
12479
+ record = (params) => {
12480
+ const record = this.recordFactory.create(params);
12481
+ this.snapshotStore.write(record);
12482
+ this.historyStore.append(record);
12483
+ return record;
12484
+ };
12485
+ get snapshotStore() {
12486
+ return this.deps.snapshotStore ?? llmUsageSnapshotStore;
12487
+ }
12488
+ get historyStore() {
12489
+ return this.deps.historyStore ?? llmUsageHistoryStore;
12490
+ }
12491
+ get recordFactory() {
12492
+ return this.deps.recordFactory ?? llmUsageRecordFactory;
12493
+ }
12494
+ };
12495
+ const llmUsageRecorder = new LlmUsageRecorder();
12496
+ //#endregion
11740
12497
  //#region src/cli/commands/ncp/create-ui-ncp-agent.ts
11741
12498
  const CODEX_RUNTIME_KIND = "codex";
11742
12499
  const CODEX_DIRECT_RUNTIME_BACKEND = "codex-sdk";
@@ -11801,6 +12558,7 @@ async function createMcpRuntimeSupport(getConfig) {
11801
12558
  };
11802
12559
  }
11803
12560
  function createNativeRuntimeFactory(params, mcpToolRegistryAdapter, assetStore, sessionCreationService, sessionRequestBroker) {
12561
+ const observedProviderManager = new ObservedProviderManager(params.providerManager, new LlmUsageObserver(llmUsageRecorder, "ui-ncp"));
11804
12562
  return ({ stateManager, sessionMetadata, setSessionMetadata }) => {
11805
12563
  const reasoningNormalizationMode = resolveNativeReasoningNormalizationMode({
11806
12564
  config: params.getConfig(),
@@ -11809,7 +12567,7 @@ function createNativeRuntimeFactory(params, mcpToolRegistryAdapter, assetStore,
11809
12567
  if (reasoningNormalizationMode !== "off" && readAssistantReasoningNormalizationModeFromMetadata(sessionMetadata) !== reasoningNormalizationMode) setSessionMetadata(writeAssistantReasoningNormalizationModeToMetadata(sessionMetadata, reasoningNormalizationMode));
11810
12568
  const toolRegistry = new NextclawNcpToolRegistry({
11811
12569
  bus: params.bus,
11812
- providerManager: params.providerManager,
12570
+ providerManager: observedProviderManager,
11813
12571
  sessionManager: params.sessionManager,
11814
12572
  cronService: params.cronService,
11815
12573
  gatewayController: params.gatewayController,
@@ -11827,7 +12585,7 @@ function createNativeRuntimeFactory(params, mcpToolRegistryAdapter, assetStore,
11827
12585
  resolveMessageToolHints: params.resolveMessageToolHints,
11828
12586
  assetStore
11829
12587
  }),
11830
- llmApi: new ProviderManagerNcpLLMApi(params.providerManager),
12588
+ llmApi: new ProviderManagerNcpLLMApi(observedProviderManager),
11831
12589
  toolRegistry,
11832
12590
  stateManager,
11833
12591
  reasoningNormalizationMode
@@ -11905,7 +12663,10 @@ async function createUiNcpAgent(params) {
11905
12663
  onSessionRunStatusChanged: params.onSessionRunStatusChanged,
11906
12664
  createRuntime: (runtimeParams) => {
11907
12665
  pluginRuntimeRegistrationController.refreshPluginRuntimeRegistrations();
11908
- return runtimeRegistry.createRuntime(runtimeParams);
12666
+ return runtimeRegistry.createRuntime({
12667
+ ...runtimeParams,
12668
+ resolveAssetContentPath: (assetUri) => assetStore.resolveContentPath(assetUri)
12669
+ });
11909
12670
  }
11910
12671
  });
11911
12672
  await backend.start();
@@ -12372,430 +13133,92 @@ var ConfigReloader = class {
12372
13133
  }
12373
13134
  };
12374
13135
  //#endregion
12375
- //#region src/cli/commands/agent/agent-runtime-pool.ts
12376
- function normalizeAgentId(value) {
12377
- return (value ?? "").trim().toLowerCase() || "main";
13136
+ //#region src/cli/commands/service-support/gateway/service-cron-job-handler.ts
13137
+ function normalizeOptionalString$2(value) {
13138
+ if (typeof value !== "string") return;
13139
+ return value.trim() || void 0;
12378
13140
  }
12379
- function normalizeEngineKind(value) {
12380
- if (typeof value !== "string") return "native";
12381
- return value.trim().toLowerCase() || "native";
13141
+ function buildCronSessionMetadata(params) {
13142
+ const { job, agentId, accountId } = params;
13143
+ const channel = normalizeOptionalString$2(job.payload.channel) ?? "cli";
13144
+ const chatId = normalizeOptionalString$2(job.payload.to) ?? "direct";
13145
+ const metadata = {
13146
+ agentId,
13147
+ agent_id: agentId,
13148
+ channel,
13149
+ chatId,
13150
+ chat_id: chatId,
13151
+ label: job.name,
13152
+ cron_job_id: job.id,
13153
+ cron_job_name: job.name,
13154
+ session_origin: "cron"
13155
+ };
13156
+ if (accountId) {
13157
+ metadata.accountId = accountId;
13158
+ metadata.account_id = accountId;
13159
+ }
13160
+ return metadata;
12382
13161
  }
12383
- function toRecord(value) {
12384
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
12385
- return value;
13162
+ function buildCronUserMessage(params) {
13163
+ const { sessionId, content, metadata } = params;
13164
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13165
+ return {
13166
+ id: `${sessionId}:user:cron:${timestamp}`,
13167
+ sessionId,
13168
+ role: "user",
13169
+ status: "final",
13170
+ timestamp,
13171
+ parts: [{
13172
+ type: "text",
13173
+ text: content
13174
+ }],
13175
+ metadata: structuredClone(metadata)
13176
+ };
12386
13177
  }
12387
- function resolveAgentProfiles(config) {
12388
- const defaults = config.agents.defaults;
12389
- const defaultAgentId = normalizeAgentId(resolveDefaultAgentProfileId(config));
12390
- const listed = resolveEffectiveAgentProfiles(config);
12391
- const seed = listed.length > 0 ? listed : [{
12392
- id: defaultAgentId,
12393
- workspace: defaults.workspace
12394
- }];
12395
- const unique = /* @__PURE__ */ new Map();
12396
- for (const entry of seed) if (!unique.has(entry.id)) unique.set(entry.id, entry);
12397
- return Array.from(unique.values()).map((entry) => ({
12398
- id: entry.id,
12399
- workspace: getWorkspacePath(entry.workspace),
12400
- model: entry.model ?? defaults.model,
12401
- engine: normalizeEngineKind(entry.engine ?? defaults.engine),
12402
- engineConfig: toRecord(entry.engineConfig) ?? toRecord(defaults.engineConfig),
12403
- maxIterations: entry.maxToolIterations ?? defaults.maxToolIterations,
12404
- contextTokens: entry.contextTokens ?? defaults.contextTokens
12405
- }));
13178
+ function extractMessageText(message) {
13179
+ return message.parts.flatMap((part) => {
13180
+ if (part.type === "text" || part.type === "rich-text") return [part.text];
13181
+ return [];
13182
+ }).map((text) => text.trim()).filter((text) => text.length > 0).join("\n\n");
12406
13183
  }
12407
- var GatewayAgentRuntimePool = class {
12408
- routeResolver;
12409
- runtimes = /* @__PURE__ */ new Map();
12410
- dynamicEngineRuntimes = /* @__PURE__ */ new Map();
12411
- resolvedProfiles = [];
12412
- running = false;
12413
- defaultAgentId = "main";
12414
- onSystemSessionUpdated = null;
12415
- constructor(options) {
12416
- this.options = options;
12417
- this.routeResolver = new AgentRouteResolver(options.config);
12418
- this.rebuild(options.config);
12419
- }
12420
- get primaryAgentId() {
12421
- return this.defaultAgentId;
12422
- }
12423
- applyRuntimeConfig(config) {
12424
- this.options.config = config;
12425
- this.options.contextConfig = config.agents.context;
12426
- this.options.execConfig = config.tools.exec;
12427
- this.options.restrictToWorkspace = config.tools.restrictToWorkspace;
12428
- this.options.searchConfig = config.search;
12429
- this.routeResolver.updateConfig(config);
12430
- this.rebuild(config);
12431
- }
12432
- applyExtensionRegistry(extensionRegistry) {
12433
- this.options.extensionRegistry = extensionRegistry;
12434
- this.rebuild(this.options.config);
12435
- }
12436
- setSystemSessionUpdatedHandler(handler) {
12437
- this.onSystemSessionUpdated = handler;
12438
- }
12439
- listAvailableEngineKinds() {
12440
- const kinds = new Set(["native"]);
12441
- for (const runtime of this.runtimes.values()) kinds.add(normalizeEngineKind(runtime.engine.kind));
12442
- for (const registration of this.options.extensionRegistry?.engines ?? []) kinds.add(normalizeEngineKind(registration.kind));
12443
- return Array.from(kinds).sort((left, right) => {
12444
- if (left === "native") return -1;
12445
- if (right === "native") return 1;
12446
- return left.localeCompare(right);
12447
- });
12448
- }
12449
- async processDirect(params) {
12450
- const { message, route } = this.resolveDirectRoute({
12451
- content: params.content,
12452
- sessionKey: params.sessionKey,
12453
- channel: params.channel,
12454
- chatId: params.chatId,
12455
- attachments: params.attachments,
12456
- metadata: params.metadata,
12457
- agentId: params.agentId
12458
- });
12459
- const forcedEngineKind = this.readForcedEngineKind(message.metadata);
12460
- const commandResult = await this.executeDirectCommand(params.content, {
12461
- channel: message.channel,
12462
- chatId: message.chatId,
12463
- sessionKey: route.sessionKey
12464
- });
12465
- if (commandResult) return commandResult;
12466
- const runtime = forcedEngineKind ? this.resolveRuntimeForEngineKind(forcedEngineKind, route.agentId) : this.resolveRuntime(route.agentId);
12467
- if (message.attachments.length > 0) return (await runtime.engine.handleInbound({
12468
- message,
12469
- sessionKey: route.sessionKey,
12470
- publishResponse: false,
12471
- onAssistantDelta: params.onAssistantDelta
12472
- }))?.content ?? "";
12473
- return runtime.engine.processDirect({
12474
- content: params.content,
12475
- sessionKey: route.sessionKey,
12476
- channel: message.channel,
12477
- chatId: message.chatId,
12478
- metadata: message.metadata,
12479
- abortSignal: params.abortSignal,
12480
- onAssistantDelta: params.onAssistantDelta,
12481
- onSessionEvent: params.onSessionEvent
12482
- });
12483
- }
12484
- async executeDirectCommand(rawContent, ctx) {
12485
- const trimmed = rawContent.trim();
12486
- if (!trimmed.startsWith("/")) return null;
12487
- const registry = new CommandRegistry(this.options.config, this.options.sessionManager);
12488
- const executeText = registry.executeText;
12489
- if (typeof executeText === "function") return (await executeText.call(registry, rawContent, {
12490
- channel: ctx.channel,
12491
- chatId: ctx.chatId,
12492
- senderId: "user",
12493
- sessionKey: ctx.sessionKey
12494
- }))?.content ?? null;
12495
- const commandRaw = trimmed.slice(1).trim();
12496
- if (!commandRaw) return null;
12497
- const [nameToken, ...restTokens] = commandRaw.split(/\s+/);
12498
- const commandName = nameToken.trim().toLowerCase();
12499
- if (!commandName) return null;
12500
- const args = parseCommandArgsFromText(commandName, restTokens.join(" ").trim(), registry.listSlashCommands());
12501
- return (await registry.execute(commandName, args, {
12502
- channel: ctx.channel,
12503
- chatId: ctx.chatId,
12504
- senderId: "user",
12505
- sessionKey: ctx.sessionKey
12506
- }))?.content ?? null;
12507
- }
12508
- supportsTurnAbort(params) {
12509
- const { route } = this.resolveDirectRoute({
12510
- content: "",
12511
- sessionKey: params.sessionKey,
12512
- channel: params.channel,
12513
- chatId: params.chatId,
12514
- metadata: params.metadata,
12515
- agentId: params.agentId
12516
- });
12517
- const forcedEngineKind = this.readForcedEngineKind(params.metadata);
12518
- let runtime;
12519
- try {
12520
- runtime = forcedEngineKind ? this.resolveRuntimeForEngineKind(forcedEngineKind, route.agentId) : this.resolveRuntime(route.agentId);
12521
- } catch (error) {
12522
- return {
12523
- supported: false,
12524
- agentId: route.agentId,
12525
- reason: error instanceof Error ? error.message : String(error)
12526
- };
12527
- }
12528
- if (!(runtime.engine.supportsAbort ?? runtime.engine.kind === "native")) return {
12529
- supported: false,
12530
- agentId: route.agentId,
12531
- reason: `engine "${runtime.engine.kind}" does not support server-side stop yet`
12532
- };
12533
- return {
12534
- supported: true,
12535
- agentId: route.agentId
12536
- };
12537
- }
12538
- async run() {
12539
- this.running = true;
12540
- while (this.running) {
12541
- const message = await this.options.bus.consumeInbound();
12542
- try {
12543
- const explicitSessionKey = this.readString(message.metadata.session_key_override);
12544
- const forcedAgentId = this.readString(message.metadata.target_agent_id);
12545
- const route = this.routeResolver.resolveInbound({
12546
- message,
12547
- forcedAgentId,
12548
- sessionKeyOverride: explicitSessionKey
12549
- });
12550
- const runtime = this.resolveRuntime(route.agentId);
12551
- if (message.channel !== "system") await this.options.bus.publishOutbound(createAssistantStreamResetControlMessage(message));
12552
- await runtime.engine.handleInbound({
12553
- message,
12554
- sessionKey: route.sessionKey,
12555
- publishResponse: true,
12556
- onAssistantDelta: message.channel !== "system" ? (delta) => {
12557
- if (!delta) return;
12558
- this.options.bus.publishOutbound(createAssistantStreamDeltaControlMessage(message, delta));
12559
- } : void 0
12560
- });
12561
- if (message.channel === "system") this.onSystemSessionUpdated?.({
12562
- sessionKey: route.sessionKey,
12563
- message
12564
- });
12565
- } catch (error) {
12566
- await this.options.bus.publishOutbound({
12567
- channel: message.channel,
12568
- chatId: message.chatId,
12569
- content: `Sorry, I encountered an error: ${formatUserFacingError(error)}`,
12570
- media: [],
12571
- metadata: {}
12572
- });
12573
- }
12574
- }
12575
- }
12576
- readString(value) {
12577
- if (typeof value !== "string") return;
12578
- return value.trim() || void 0;
12579
- }
12580
- resolveDirectRoute(params) {
12581
- const message = {
12582
- channel: params.channel ?? "cli",
12583
- senderId: "user",
12584
- chatId: params.chatId ?? "direct",
12585
- content: params.content,
12586
- timestamp: /* @__PURE__ */ new Date(),
12587
- attachments: params.attachments ?? [],
12588
- metadata: params.metadata ?? {}
12589
- };
12590
- const forcedAgentId = this.readString(params.agentId) ?? parseAgentScopedSessionKey(params.sessionKey)?.agentId ?? void 0;
12591
- return {
12592
- message,
12593
- route: this.routeResolver.resolveInbound({
12594
- message,
12595
- forcedAgentId,
12596
- sessionKeyOverride: params.sessionKey
12597
- })
12598
- };
12599
- }
12600
- resolveRuntime(agentId) {
12601
- const normalized = normalizeAgentId(agentId);
12602
- const runtime = this.runtimes.get(normalized);
12603
- if (runtime) return runtime;
12604
- const fallback = this.runtimes.get(this.defaultAgentId);
12605
- if (fallback) return fallback;
12606
- throw new Error("No agent runtime available");
12607
- }
12608
- readForcedEngineKind(metadata) {
12609
- if (!metadata || typeof metadata !== "object") return;
12610
- const raw = this.readString(metadata.session_type) ?? this.readString(metadata.sessionType) ?? this.readString(metadata.engine_kind) ?? this.readString(metadata.engineKind);
12611
- return raw ? normalizeEngineKind(raw) : void 0;
12612
- }
12613
- findRuntimeByEngineKind(kind, preferredAgentId) {
12614
- const normalizedKind = normalizeEngineKind(kind);
12615
- const preferred = preferredAgentId ? this.runtimes.get(normalizeAgentId(preferredAgentId)) : null;
12616
- if (preferred && normalizeEngineKind(preferred.engine.kind) === normalizedKind) return preferred;
12617
- for (const runtime of this.runtimes.values()) if (normalizeEngineKind(runtime.engine.kind) === normalizedKind) return runtime;
12618
- return null;
12619
- }
12620
- resolveBaseProfileForDynamicEngine(agentId) {
12621
- const normalizedAgentId = normalizeAgentId(agentId);
12622
- return this.resolvedProfiles.find((profile) => profile.id === normalizedAgentId) ?? this.resolvedProfiles.find((profile) => profile.id === this.defaultAgentId) ?? this.resolvedProfiles[0] ?? {
12623
- id: this.defaultAgentId,
12624
- workspace: getWorkspacePath(this.options.config.agents.defaults.workspace),
12625
- model: this.options.config.agents.defaults.model,
12626
- maxIterations: this.options.config.agents.defaults.maxToolIterations,
12627
- contextTokens: this.options.config.agents.defaults.contextTokens,
12628
- engine: "native",
12629
- engineConfig: toRecord(this.options.config.agents.defaults.engineConfig)
12630
- };
12631
- }
12632
- resolveRuntimeForEngineKind(kind, fallbackAgentId) {
12633
- const normalizedKind = normalizeEngineKind(kind);
12634
- const existing = this.findRuntimeByEngineKind(normalizedKind, fallbackAgentId);
12635
- if (existing) return existing;
12636
- if (!this.listAvailableEngineKinds().includes(normalizedKind)) throw new Error(`engine "${normalizedKind}" is not available`);
12637
- const cached = this.dynamicEngineRuntimes.get(normalizedKind);
12638
- if (cached) return cached;
12639
- const dynamicProfile = {
12640
- ...this.resolveBaseProfileForDynamicEngine(fallbackAgentId),
12641
- id: `__session_engine__${normalizedKind}`,
12642
- engine: normalizedKind
12643
- };
12644
- const runtime = {
12645
- id: dynamicProfile.id,
12646
- engine: this.createEngine(dynamicProfile, this.options.config)
12647
- };
12648
- this.dynamicEngineRuntimes.set(normalizedKind, runtime);
12649
- return runtime;
12650
- }
12651
- createNativeEngineFactory() {
12652
- return (context) => new NativeAgentEngine({
12653
- bus: context.bus,
12654
- providerManager: context.providerManager,
12655
- workspace: context.workspace,
12656
- model: context.model,
12657
- maxIterations: context.maxIterations,
12658
- contextTokens: context.contextTokens,
12659
- searchConfig: context.searchConfig,
12660
- execConfig: context.execConfig,
12661
- cronService: context.cronService,
12662
- restrictToWorkspace: context.restrictToWorkspace,
12663
- sessionManager: context.sessionManager,
12664
- contextConfig: context.contextConfig,
12665
- gatewayController: context.gatewayController,
12666
- config: context.config,
12667
- extensionRegistry: context.extensionRegistry,
12668
- resolveMessageToolHints: context.resolveMessageToolHints,
12669
- agentId: context.agentId
12670
- });
12671
- }
12672
- resolveEngineFactory(kind) {
12673
- if (kind === "native") return this.createNativeEngineFactory();
12674
- const matched = (this.options.extensionRegistry?.engines ?? []).find((entry) => normalizeEngineKind(entry.kind) === kind);
12675
- if (matched) return matched.factory;
12676
- console.warn(`[engine] unknown engine "${kind}", fallback to "native"`);
12677
- return this.createNativeEngineFactory();
12678
- }
12679
- createEngine(profile, config) {
12680
- const kind = normalizeEngineKind(profile.engine);
12681
- const factory = this.resolveEngineFactory(kind);
12682
- const context = {
12683
- agentId: profile.id,
12684
- workspace: profile.workspace,
12685
- model: profile.model,
12686
- maxIterations: profile.maxIterations,
12687
- contextTokens: profile.contextTokens,
12688
- engineConfig: profile.engineConfig,
12689
- bus: this.options.bus,
12690
- providerManager: this.options.providerManager,
12691
- sessionManager: this.options.sessionManager,
12692
- cronService: this.options.cronService,
12693
- restrictToWorkspace: this.options.restrictToWorkspace,
12694
- searchConfig: this.options.searchConfig,
12695
- execConfig: this.options.execConfig,
12696
- contextConfig: this.options.contextConfig,
12697
- gatewayController: this.options.gatewayController,
12698
- config,
12699
- extensionRegistry: this.options.extensionRegistry,
12700
- resolveMessageToolHints: this.options.resolveMessageToolHints
12701
- };
12702
- try {
12703
- return factory(context);
12704
- } catch (error) {
12705
- if (kind === "native") throw error;
12706
- console.warn(`[engine] failed to create "${kind}" for agent "${profile.id}": ${String(error)}`);
12707
- return this.createNativeEngineFactory()(context);
12708
- }
12709
- }
12710
- rebuild(config) {
12711
- const profiles = resolveAgentProfiles(config);
12712
- this.resolvedProfiles = profiles;
12713
- this.defaultAgentId = this.readString(config.agents.list.find((entry) => entry.default)?.id) ?? profiles[0]?.id ?? "main";
12714
- const nextRuntimes = /* @__PURE__ */ new Map();
12715
- for (const profile of profiles) {
12716
- const engine = this.createEngine(profile, config);
12717
- nextRuntimes.set(profile.id, {
12718
- id: profile.id,
12719
- engine
12720
- });
12721
- }
12722
- this.runtimes = nextRuntimes;
12723
- this.dynamicEngineRuntimes.clear();
12724
- }
12725
- };
12726
- function parseCommandArgsFromText(commandName, rawTail, specs) {
12727
- if (!rawTail) return {};
12728
- const options = specs.find((item) => item.name.trim().toLowerCase() === commandName)?.options;
12729
- if (!options || options.length === 0) return {};
12730
- const tokens = rawTail.split(/\s+/).filter(Boolean);
12731
- const args = {};
12732
- let cursor = 0;
12733
- for (let i = 0; i < options.length; i += 1) {
12734
- if (cursor >= tokens.length) break;
12735
- const option = options[i];
12736
- const isLastOption = i === options.length - 1;
12737
- const rawValue = isLastOption ? tokens.slice(cursor).join(" ") : tokens[cursor];
12738
- cursor += isLastOption ? tokens.length - cursor : 1;
12739
- const parsedValue = parseCommandOptionValue(option.type, rawValue);
12740
- if (parsedValue !== void 0) args[option.name] = parsedValue;
12741
- }
12742
- return args;
12743
- }
12744
- function parseCommandOptionValue(type, rawValue) {
12745
- const value = rawValue.trim();
12746
- if (!value) return;
12747
- if (type === "number") {
12748
- const parsed = Number(value);
12749
- return Number.isFinite(parsed) ? parsed : void 0;
12750
- }
12751
- if (type === "boolean") {
12752
- const lowered = value.toLowerCase();
12753
- if ([
12754
- "1",
12755
- "true",
12756
- "yes",
12757
- "on"
12758
- ].includes(lowered)) return true;
12759
- if ([
12760
- "0",
12761
- "false",
12762
- "no",
12763
- "off"
12764
- ].includes(lowered)) return false;
12765
- return;
13184
+ async function runJobOverNcp(params) {
13185
+ const { agent, sessionId, message, metadata, missingCompletedMessageError, runErrorMessage } = params;
13186
+ let completedMessage;
13187
+ for await (const event of agent.runApi.send({
13188
+ sessionId,
13189
+ message,
13190
+ metadata
13191
+ })) {
13192
+ if (event.type === NcpEventType.MessageFailed) throw new Error(event.payload.error.message);
13193
+ if (event.type === NcpEventType.RunError) throw new Error(event.payload.error ?? runErrorMessage);
13194
+ if (event.type === NcpEventType.MessageCompleted) completedMessage = event.payload.message;
12766
13195
  }
12767
- return value;
12768
- }
12769
- function formatUserFacingError(error, maxChars = 320) {
12770
- const normalized = (error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error")).replace(/\s+/g, " ").trim();
12771
- if (!normalized) return "Unknown error";
12772
- if (normalized.length <= maxChars) return normalized;
12773
- return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
12774
- }
12775
- //#endregion
12776
- //#region src/cli/commands/service-support/gateway/service-cron-job-handler.ts
12777
- function normalizeOptionalString(value) {
12778
- if (typeof value !== "string") return;
12779
- return value.trim() || void 0;
12780
- }
12781
- function buildCronJobMetadata(accountId) {
12782
- if (!accountId) return {};
12783
- return {
12784
- accountId,
12785
- account_id: accountId
12786
- };
13196
+ if (!completedMessage) throw new Error(missingCompletedMessageError);
13197
+ return extractMessageText(completedMessage);
12787
13198
  }
12788
13199
  function createCronJobHandler(params) {
12789
13200
  return async (job) => {
12790
- const metadata = buildCronJobMetadata(normalizeOptionalString(job.payload.accountId));
12791
- const agentId = normalizeOptionalString(job.payload.agentId) ?? params.runtimePool.primaryAgentId;
12792
- const response = await params.runtimePool.processDirect({
12793
- content: job.payload.message,
12794
- sessionKey: `cron:${job.id}`,
12795
- channel: job.payload.channel ?? "cli",
12796
- chatId: job.payload.to ?? "direct",
13201
+ const ncpAgent = params.resolveNcpAgent();
13202
+ if (!ncpAgent) throw new Error("NCP agent is not ready for cron execution.");
13203
+ const accountId = normalizeOptionalString$2(job.payload.accountId);
13204
+ const agentId = normalizeOptionalString$2(job.payload.agentId) ?? "main";
13205
+ const sessionId = `cron:${job.id}`;
13206
+ const metadata = buildCronSessionMetadata({
13207
+ job,
13208
+ agentId,
13209
+ accountId
13210
+ });
13211
+ const response = await runJobOverNcp({
13212
+ agent: ncpAgent,
13213
+ sessionId,
13214
+ message: buildCronUserMessage({
13215
+ sessionId,
13216
+ content: job.payload.message,
13217
+ metadata
13218
+ }),
12797
13219
  metadata,
12798
- agentId
13220
+ missingCompletedMessageError: "cron job completed without a final assistant message",
13221
+ runErrorMessage: "cron job failed"
12799
13222
  });
12800
13223
  if (job.payload.deliver && job.payload.to) await params.bus.publishOutbound({
12801
13224
  channel: job.payload.channel ?? "cli",
@@ -12807,21 +13230,81 @@ function createCronJobHandler(params) {
12807
13230
  return response;
12808
13231
  };
12809
13232
  }
13233
+ function buildHeartbeatMetadata(params) {
13234
+ return {
13235
+ agentId: params.agentId,
13236
+ agent_id: params.agentId,
13237
+ channel: "cli",
13238
+ chatId: "direct",
13239
+ chat_id: "direct",
13240
+ label: "heartbeat",
13241
+ session_origin: "heartbeat"
13242
+ };
13243
+ }
13244
+ function buildHeartbeatUserMessage(params) {
13245
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13246
+ return {
13247
+ id: `heartbeat:user:${timestamp}`,
13248
+ sessionId: "heartbeat",
13249
+ role: "user",
13250
+ status: "final",
13251
+ timestamp,
13252
+ parts: [{
13253
+ type: "text",
13254
+ text: params.content
13255
+ }],
13256
+ metadata: structuredClone(params.metadata)
13257
+ };
13258
+ }
13259
+ function createHeartbeatJobHandler(params) {
13260
+ return async (prompt) => {
13261
+ const ncpAgent = params.resolveNcpAgent();
13262
+ if (!ncpAgent) throw new Error("NCP agent is not ready for heartbeat execution.");
13263
+ const metadata = buildHeartbeatMetadata({ agentId: params.resolveAgentId() });
13264
+ return await runJobOverNcp({
13265
+ agent: ncpAgent,
13266
+ sessionId: "heartbeat",
13267
+ message: buildHeartbeatUserMessage({
13268
+ content: prompt,
13269
+ metadata
13270
+ }),
13271
+ metadata,
13272
+ missingCompletedMessageError: "heartbeat execution completed without a final assistant message",
13273
+ runErrorMessage: "heartbeat execution failed"
13274
+ });
13275
+ };
13276
+ }
12810
13277
  //#endregion
12811
13278
  //#region src/cli/commands/service-support/gateway/service-gateway-context.ts
12812
- const { ChannelManager: ChannelManager$1, CronService: CronService$1, getConfigPath: getConfigPath$2, getDataDir: getDataDir$1, getWorkspacePath: getWorkspacePath$2, HeartbeatService, loadConfig: loadConfig$3, MessageBus: MessageBus$2, ProviderManager: ProviderManager$1, resolveConfigSecrets: resolveConfigSecrets$3, saveConfig: saveConfig$1, SessionManager: SessionManager$1 } = NextclawCore;
13279
+ const { ChannelManager: ChannelManager$1, CronService: CronService$1, getConfigPath: getConfigPath$2, getDataDir: getDataDir$1, getWorkspacePath: getWorkspacePath$2, HeartbeatService, loadConfig: loadConfig$3, MessageBus: MessageBus$2, ProviderManager: ProviderManager$1, resolveConfigSecrets: resolveConfigSecrets$3, resolveDefaultAgentProfileId: resolveDefaultAgentProfileId$1, saveConfig: saveConfig$1, SessionManager: SessionManager$2 } = NextclawCore;
12813
13280
  function applyGatewayCapabilityState(gateway, next) {
12814
13281
  gateway.pluginRegistry = next.pluginRegistry;
12815
13282
  gateway.pluginChannelBindings = next.pluginChannelBindings;
12816
13283
  gateway.extensionRegistry = next.extensionRegistry;
12817
13284
  }
13285
+ function normalizeAgentId(value) {
13286
+ return (value ?? "").trim().toLowerCase() || "main";
13287
+ }
13288
+ function createGatewayHeartbeat(state, params) {
13289
+ const handleHeartbeat = createHeartbeatJobHandler({
13290
+ resolveNcpAgent: () => params.getLiveUiNcpAgent?.() ?? null,
13291
+ resolveAgentId: () => normalizeAgentId(resolveDefaultAgentProfileId$1(resolveConfigSecrets$3(loadConfig$3(), { configPath: state.runtimeConfigPath })))
13292
+ });
13293
+ return new HeartbeatService(state.workspace, async (promptText) => await handleHeartbeat(promptText), 1800, true);
13294
+ }
13295
+ function createGatewayCronJobHandler(params) {
13296
+ return createCronJobHandler({
13297
+ resolveNcpAgent: () => params.getLiveUiNcpAgent?.() ?? null,
13298
+ bus: params.bus
13299
+ });
13300
+ }
12818
13301
  function createGatewayShellContext(params) {
12819
13302
  const runtimeConfigPath = getConfigPath$2();
12820
13303
  const config = resolveConfigSecrets$3(loadConfig$3(), { configPath: runtimeConfigPath });
12821
13304
  const workspace = getWorkspacePath$2(config.agents.defaults.workspace);
12822
13305
  const homeDir = getDataDir$1();
12823
13306
  const cronStorePath = join(getDataDir$1(), "cron", "jobs.json");
12824
- const sessionManager = measureStartupSync("service.gateway_shell_context.session_manager", () => new SessionManager$1({
13307
+ const sessionManager = measureStartupSync("service.gateway_shell_context.session_manager", () => new SessionManager$2({
12825
13308
  workspace,
12826
13309
  homeDir
12827
13310
  }));
@@ -12842,10 +13325,11 @@ function createGatewayShellContext(params) {
12842
13325
  };
12843
13326
  }
12844
13327
  function createGatewayStartupContext(params) {
13328
+ const { shellContext: providedShellContext, uiOverrides, allowMissingProvider, uiStaticDir, initialPluginRegistry, makeProvider, makeMissingProvider, requestRestart, getLiveUiNcpAgent } = params;
12845
13329
  const state = {};
12846
- const shellContext = params.shellContext ?? createGatewayShellContext({
12847
- uiOverrides: params.uiOverrides,
12848
- uiStaticDir: params.uiStaticDir
13330
+ const shellContext = providedShellContext ?? createGatewayShellContext({
13331
+ uiOverrides,
13332
+ uiStaticDir
12849
13333
  });
12850
13334
  state.runtimeConfigPath = shellContext.runtimeConfigPath;
12851
13335
  state.config = shellContext.config;
@@ -12855,14 +13339,14 @@ function createGatewayStartupContext(params) {
12855
13339
  state.uiConfig = shellContext.uiConfig;
12856
13340
  state.uiStaticDir = shellContext.uiStaticDir;
12857
13341
  state.remoteModule = shellContext.remoteModule;
12858
- state.pluginRegistry = params.initialPluginRegistry ?? measureStartupSync("service.gateway_context.load_plugin_registry", () => loadPluginRegistry(state.config, state.workspace));
13342
+ state.pluginRegistry = initialPluginRegistry ?? measureStartupSync("service.gateway_context.load_plugin_registry", () => loadPluginRegistry(state.config, state.workspace));
12859
13343
  state.pluginChannelBindings = measureStartupSync("service.gateway_context.get_plugin_channel_bindings", () => getPluginChannelBindings(state.pluginRegistry));
12860
13344
  state.extensionRegistry = measureStartupSync("service.gateway_context.to_extension_registry", () => toExtensionRegistry(state.pluginRegistry));
12861
13345
  logPluginDiagnostics(state.pluginRegistry);
12862
13346
  state.bus = new MessageBus$2();
12863
- const provider = params.allowMissingProvider === true ? params.makeProvider(state.config, { allowMissing: true }) : params.makeProvider(state.config);
13347
+ const provider = allowMissingProvider === true ? makeProvider(state.config, { allowMissing: true }) : makeProvider(state.config);
12864
13348
  state.providerManager = measureStartupSync("service.gateway_context.provider_manager", () => new ProviderManager$1({
12865
- defaultProvider: provider ?? params.makeMissingProvider(state.config),
13349
+ defaultProvider: provider ?? makeMissingProvider(state.config),
12866
13350
  config: state.config
12867
13351
  }));
12868
13352
  if (!provider) console.warn("Warning: No API key configured. The gateway is running, but agent replies are disabled until provider config is set.");
@@ -12873,12 +13357,12 @@ function createGatewayStartupContext(params) {
12873
13357
  bus: state.bus,
12874
13358
  sessionManager: state.sessionManager,
12875
13359
  providerManager: state.providerManager,
12876
- makeProvider: (nextConfig) => params.makeProvider(nextConfig, { allowMissing: true }) ?? params.makeMissingProvider(nextConfig),
13360
+ makeProvider: (nextConfig) => makeProvider(nextConfig, { allowMissing: true }) ?? makeMissingProvider(nextConfig),
12877
13361
  loadConfig: () => resolveConfigSecrets$3(loadConfig$3(), { configPath: state.runtimeConfigPath }),
12878
13362
  resolveChannelConfig: (nextConfig) => resolveChannelConfigView(nextConfig, state.pluginChannelBindings),
12879
13363
  getExtensionChannels: () => state.extensionRegistry.channels,
12880
13364
  onRestartRequired: (paths) => {
12881
- params.requestRestart({
13365
+ requestRestart({
12882
13366
  reason: `config reload requires restart: ${paths.join(", ")}`,
12883
13367
  manualMessage: `Config changes require restart: ${paths.join(", ")}`,
12884
13368
  strategy: "background-service-or-manual"
@@ -12895,7 +13379,7 @@ function createGatewayStartupContext(params) {
12895
13379
  getConfigPath: getConfigPath$2,
12896
13380
  saveConfig: saveConfig$1,
12897
13381
  requestRestart: async (options) => {
12898
- await params.requestRestart({
13382
+ await requestRestart({
12899
13383
  reason: options?.reason ?? "gateway tool restart",
12900
13384
  manualMessage: "Restart the gateway to apply changes.",
12901
13385
  strategy: "background-service-or-exit",
@@ -12904,37 +13388,356 @@ function createGatewayStartupContext(params) {
12904
13388
  });
12905
13389
  }
12906
13390
  }));
12907
- state.runtimePool = measureStartupSync("service.gateway_context.runtime_pool", () => new GatewayAgentRuntimePool({
13391
+ state.cron.onJob = createGatewayCronJobHandler({
12908
13392
  bus: state.bus,
12909
- providerManager: state.providerManager,
12910
- sessionManager: state.sessionManager,
12911
- config: state.config,
12912
- cronService: state.cron,
12913
- restrictToWorkspace: state.config.tools.restrictToWorkspace,
12914
- searchConfig: state.config.search,
12915
- execConfig: state.config.tools.exec,
12916
- contextConfig: state.config.agents.context,
12917
- gatewayController: state.gatewayController,
12918
- extensionRegistry: state.extensionRegistry,
12919
- resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
12920
- registry: state.pluginRegistry,
12921
- channel,
12922
- cfg: resolveConfigSecrets$3(loadConfig$3(), { configPath: state.runtimeConfigPath }),
12923
- accountId
12924
- })
12925
- }));
12926
- state.cron.onJob = createCronJobHandler({
12927
- runtimePool: state.runtimePool,
12928
- bus: state.bus
13393
+ getLiveUiNcpAgent
12929
13394
  });
12930
- state.heartbeat = new HeartbeatService(state.workspace, async (promptText) => state.runtimePool.processDirect({
12931
- content: promptText,
12932
- sessionKey: "heartbeat",
12933
- agentId: state.runtimePool.primaryAgentId
12934
- }), 1800, true);
13395
+ state.heartbeat = createGatewayHeartbeat(state, { getLiveUiNcpAgent });
12935
13396
  return state;
12936
13397
  }
12937
13398
  //#endregion
13399
+ //#region src/cli/commands/ncp/runtime/nextclaw-ncp-runner.ts
13400
+ function normalizeOptionalString$1(value) {
13401
+ if (typeof value !== "string") return;
13402
+ return value.trim() || void 0;
13403
+ }
13404
+ function resolveAttachmentName(attachment) {
13405
+ const explicitName = normalizeOptionalString$1(attachment.name);
13406
+ if (explicitName) return explicitName;
13407
+ const explicitPath = normalizeOptionalString$1(attachment.path);
13408
+ if (explicitPath) return basename(explicitPath);
13409
+ const explicitUrl = normalizeOptionalString$1(attachment.url);
13410
+ if (explicitUrl) try {
13411
+ return basename(new URL(explicitUrl).pathname) || "asset.bin";
13412
+ } catch {
13413
+ return basename(explicitUrl) || "asset.bin";
13414
+ }
13415
+ return "asset.bin";
13416
+ }
13417
+ async function attachmentToPart(attachment, assetApi) {
13418
+ const assetUri = normalizeOptionalString$1(attachment.assetUri);
13419
+ if (assetUri) return createFilePartFromAssetUri(attachment, assetUri);
13420
+ const remoteUrl = normalizeOptionalString$1(attachment.url);
13421
+ const localPath = normalizeOptionalString$1(attachment.path);
13422
+ if (localPath) return await createFilePartFromLocalPath(attachment, localPath, assetApi);
13423
+ if (remoteUrl) return createFilePartFromRemoteUrl(attachment, remoteUrl);
13424
+ throw new Error(`Unsupported attachment payload for "${resolveAttachmentName(attachment)}".`);
13425
+ }
13426
+ function createBaseFilePart(attachment) {
13427
+ return {
13428
+ ...attachment.name ? { name: attachment.name } : {},
13429
+ ...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
13430
+ ...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
13431
+ };
13432
+ }
13433
+ function createFilePartFromAssetUri(attachment, assetUri) {
13434
+ return {
13435
+ type: "file",
13436
+ assetUri,
13437
+ ...createBaseFilePart(attachment)
13438
+ };
13439
+ }
13440
+ async function createFilePartFromLocalPath(attachment, localPath, assetApi) {
13441
+ if (!assetApi) throw new Error("NCP asset api is unavailable for local attachments.");
13442
+ const fileName = resolveAttachmentName(attachment);
13443
+ const bytes = await readFile(localPath);
13444
+ return {
13445
+ type: "file",
13446
+ assetUri: (await assetApi.put({
13447
+ fileName,
13448
+ mimeType: attachment.mimeType ?? null,
13449
+ bytes
13450
+ })).uri,
13451
+ name: attachment.name ?? fileName,
13452
+ ...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
13453
+ ...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
13454
+ };
13455
+ }
13456
+ function createFilePartFromRemoteUrl(attachment, remoteUrl) {
13457
+ return {
13458
+ type: "file",
13459
+ url: remoteUrl,
13460
+ name: attachment.name ?? resolveAttachmentName(attachment),
13461
+ ...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
13462
+ ...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
13463
+ };
13464
+ }
13465
+ async function buildUserMessageParts(params) {
13466
+ const parts = [];
13467
+ if (params.content.length > 0) parts.push({
13468
+ type: "text",
13469
+ text: params.content
13470
+ });
13471
+ for (const attachment of params.attachments ?? []) parts.push(await attachmentToPart(attachment, params.assetApi));
13472
+ return parts;
13473
+ }
13474
+ async function buildNcpUserMessage(params) {
13475
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13476
+ return {
13477
+ id: `${params.sessionId}:user:${timestamp}`,
13478
+ sessionId: params.sessionId,
13479
+ role: "user",
13480
+ status: "final",
13481
+ timestamp,
13482
+ parts: await buildUserMessageParts({
13483
+ content: params.content,
13484
+ attachments: params.attachments,
13485
+ assetApi: params.assetApi
13486
+ }),
13487
+ metadata: structuredClone(params.metadata ?? {})
13488
+ };
13489
+ }
13490
+ async function runPromptOverNcp(params) {
13491
+ const message = await buildNcpUserMessage({
13492
+ sessionId: params.sessionId,
13493
+ content: params.content,
13494
+ attachments: params.attachments,
13495
+ metadata: params.metadata,
13496
+ assetApi: params.agent.assetApi
13497
+ });
13498
+ let completedMessage;
13499
+ for await (const event of params.agent.runApi.send({
13500
+ sessionId: params.sessionId,
13501
+ message,
13502
+ metadata: params.metadata
13503
+ }, { ...params.abortSignal ? { signal: params.abortSignal } : {} })) {
13504
+ params.onEvent?.(event);
13505
+ if (event.type === NcpEventType.MessageTextDelta) {
13506
+ params.onAssistantDelta?.(event.payload.delta);
13507
+ continue;
13508
+ }
13509
+ if (event.type === NcpEventType.MessageFailed) throw new Error(event.payload.error.message);
13510
+ if (event.type === NcpEventType.RunError) throw new Error(event.payload.error ?? params.runErrorMessage ?? "NCP run failed.");
13511
+ if (event.type === NcpEventType.MessageCompleted) completedMessage = event.payload.message;
13512
+ }
13513
+ if (!completedMessage) throw new Error(params.missingCompletedMessageError ?? "NCP run completed without a final assistant message.");
13514
+ return {
13515
+ text: extractTextFromNcpMessage(completedMessage),
13516
+ completedMessage
13517
+ };
13518
+ }
13519
+ //#endregion
13520
+ //#region src/cli/commands/ncp/runtime/nextclaw-ncp-dispatch.ts
13521
+ function normalizeOptionalString(value) {
13522
+ if (typeof value !== "string") return;
13523
+ return value.trim() || void 0;
13524
+ }
13525
+ function requireNcpAgent(resolveNcpAgent, purpose) {
13526
+ const agent = resolveNcpAgent?.() ?? null;
13527
+ if (!agent) throw new Error(`NCP agent is not ready for ${purpose}.`);
13528
+ return agent;
13529
+ }
13530
+ function createDirectInboundMessage(params) {
13531
+ return {
13532
+ channel: params.channel ?? "cli",
13533
+ senderId: "user",
13534
+ chatId: params.chatId ?? "direct",
13535
+ content: params.content,
13536
+ timestamp: /* @__PURE__ */ new Date(),
13537
+ attachments: params.attachments ?? [],
13538
+ metadata: structuredClone(params.metadata ?? {})
13539
+ };
13540
+ }
13541
+ function buildRunMetadata(params) {
13542
+ return {
13543
+ ...params.message.metadata ?? {},
13544
+ ...params.metadata ?? {},
13545
+ channel: params.message.channel,
13546
+ chatId: params.message.chatId,
13547
+ chat_id: params.message.chatId,
13548
+ accountId: params.route.accountId,
13549
+ account_id: params.route.accountId,
13550
+ agentId: params.route.agentId,
13551
+ agent_id: params.route.agentId,
13552
+ sessionKey: params.route.sessionKey,
13553
+ session_key: params.route.sessionKey,
13554
+ senderId: params.message.senderId,
13555
+ sender_id: params.message.senderId
13556
+ };
13557
+ }
13558
+ function parseCommandOptionValue(type, rawValue) {
13559
+ const value = rawValue.trim();
13560
+ if (!value) return;
13561
+ if (type === "number") {
13562
+ const parsed = Number(value);
13563
+ return Number.isFinite(parsed) ? parsed : void 0;
13564
+ }
13565
+ if (type === "boolean") {
13566
+ const lowered = value.toLowerCase();
13567
+ if ([
13568
+ "1",
13569
+ "true",
13570
+ "yes",
13571
+ "on"
13572
+ ].includes(lowered)) return true;
13573
+ if ([
13574
+ "0",
13575
+ "false",
13576
+ "no",
13577
+ "off"
13578
+ ].includes(lowered)) return false;
13579
+ return;
13580
+ }
13581
+ return value;
13582
+ }
13583
+ function parseCommandArgsFromText(commandName, rawTail, specs) {
13584
+ if (!rawTail) return {};
13585
+ const options = specs.find((item) => item.name.trim().toLowerCase() === commandName)?.options;
13586
+ if (!options || options.length === 0) return {};
13587
+ const tokens = rawTail.split(/\s+/).filter(Boolean);
13588
+ const args = {};
13589
+ let cursor = 0;
13590
+ for (let index = 0; index < options.length; index += 1) {
13591
+ if (cursor >= tokens.length) break;
13592
+ const option = options[index];
13593
+ const isLastOption = index === options.length - 1;
13594
+ const rawValue = isLastOption ? tokens.slice(cursor).join(" ") : tokens[cursor];
13595
+ cursor += isLastOption ? tokens.length - cursor : 1;
13596
+ const parsedValue = parseCommandOptionValue(option.type, rawValue);
13597
+ if (parsedValue !== void 0) args[option.name] = parsedValue;
13598
+ }
13599
+ return args;
13600
+ }
13601
+ async function executeSlashCommandMaybe(params) {
13602
+ const trimmed = params.rawContent.trim();
13603
+ if (!trimmed.startsWith("/")) return null;
13604
+ const registry = new CommandRegistry(params.config, params.sessionManager);
13605
+ const executeText = registry.executeText;
13606
+ if (typeof executeText === "function") return (await executeText.call(registry, params.rawContent, {
13607
+ channel: params.channel,
13608
+ chatId: params.chatId,
13609
+ senderId: "user",
13610
+ sessionKey: params.sessionKey
13611
+ }))?.content ?? null;
13612
+ const commandRaw = trimmed.slice(1).trim();
13613
+ if (!commandRaw) return null;
13614
+ const [nameToken, ...restTokens] = commandRaw.split(/\s+/);
13615
+ const commandName = nameToken.trim().toLowerCase();
13616
+ if (!commandName) return null;
13617
+ const args = parseCommandArgsFromText(commandName, restTokens.join(" ").trim(), registry.listSlashCommands());
13618
+ return (await registry.execute(commandName, args, {
13619
+ channel: params.channel,
13620
+ chatId: params.chatId,
13621
+ senderId: "user",
13622
+ sessionKey: params.sessionKey
13623
+ }))?.content ?? null;
13624
+ }
13625
+ function resolveDirectRoute(params) {
13626
+ const message = createDirectInboundMessage(params);
13627
+ const forcedAgentId = normalizeOptionalString(params.agentId) ?? parseAgentScopedSessionKey(params.sessionKey)?.agentId ?? void 0;
13628
+ return {
13629
+ message,
13630
+ route: new AgentRouteResolver(params.config).resolveInbound({
13631
+ message,
13632
+ forcedAgentId,
13633
+ sessionKeyOverride: params.sessionKey
13634
+ })
13635
+ };
13636
+ }
13637
+ function formatUserFacingError(error, maxChars = 320) {
13638
+ const normalized = (error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error")).replace(/\s+/g, " ").trim();
13639
+ if (!normalized) return "Unknown error";
13640
+ if (normalized.length <= maxChars) return normalized;
13641
+ return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
13642
+ }
13643
+ async function dispatchPromptOverNcp(params) {
13644
+ const { message, route } = resolveDirectRoute({
13645
+ config: params.config,
13646
+ content: params.content,
13647
+ sessionKey: params.sessionKey,
13648
+ channel: params.channel,
13649
+ chatId: params.chatId,
13650
+ attachments: params.attachments,
13651
+ metadata: params.metadata,
13652
+ agentId: params.agentId
13653
+ });
13654
+ const commandResult = await executeSlashCommandMaybe({
13655
+ config: params.config,
13656
+ sessionManager: params.sessionManager,
13657
+ rawContent: params.content,
13658
+ channel: message.channel,
13659
+ chatId: message.chatId,
13660
+ sessionKey: route.sessionKey
13661
+ });
13662
+ if (commandResult) return commandResult;
13663
+ return (await runPromptOverNcp({
13664
+ agent: requireNcpAgent(params.resolveNcpAgent, "direct dispatch"),
13665
+ sessionId: route.sessionKey,
13666
+ content: params.content,
13667
+ attachments: params.attachments,
13668
+ metadata: buildRunMetadata({
13669
+ message,
13670
+ route
13671
+ }),
13672
+ abortSignal: params.abortSignal,
13673
+ onAssistantDelta: params.onAssistantDelta,
13674
+ missingCompletedMessageError: `session "${route.sessionKey}" completed without a final assistant message`,
13675
+ runErrorMessage: `session "${route.sessionKey}" failed`
13676
+ })).text;
13677
+ }
13678
+ async function runGatewayInboundLoop(params) {
13679
+ while (true) {
13680
+ const message = await params.bus.consumeInbound();
13681
+ try {
13682
+ const explicitSessionKey = normalizeOptionalString(message.metadata.session_key_override);
13683
+ const forcedAgentId = normalizeOptionalString(message.metadata.target_agent_id);
13684
+ const route = new AgentRouteResolver(params.getConfig()).resolveInbound({
13685
+ message,
13686
+ forcedAgentId,
13687
+ sessionKeyOverride: explicitSessionKey
13688
+ });
13689
+ const agent = requireNcpAgent(params.resolveNcpAgent, "gateway dispatch");
13690
+ if (message.channel !== "system") await params.bus.publishOutbound(createAssistantStreamResetControlMessage(message));
13691
+ const result = await runPromptOverNcp({
13692
+ agent,
13693
+ sessionId: route.sessionKey,
13694
+ content: message.content,
13695
+ attachments: message.attachments,
13696
+ metadata: buildRunMetadata({
13697
+ message,
13698
+ route
13699
+ }),
13700
+ onAssistantDelta: message.channel !== "system" ? (delta) => {
13701
+ if (!delta) return;
13702
+ params.bus.publishOutbound(createAssistantStreamDeltaControlMessage(message, delta));
13703
+ } : void 0,
13704
+ missingCompletedMessageError: `session "${route.sessionKey}" completed without a final assistant message`,
13705
+ runErrorMessage: `session "${route.sessionKey}" failed`
13706
+ });
13707
+ if (message.channel === "system") {
13708
+ params.onSystemSessionUpdated?.({
13709
+ sessionKey: route.sessionKey,
13710
+ message
13711
+ });
13712
+ continue;
13713
+ }
13714
+ if (!result.text.trim()) {
13715
+ await params.bus.publishOutbound(createTypingStopControlMessage(message));
13716
+ continue;
13717
+ }
13718
+ await params.bus.publishOutbound({
13719
+ channel: message.channel,
13720
+ chatId: message.chatId,
13721
+ content: result.text,
13722
+ media: [],
13723
+ metadata: buildRunMetadata({
13724
+ message,
13725
+ route,
13726
+ metadata: result.completedMessage.metadata
13727
+ })
13728
+ });
13729
+ } catch (error) {
13730
+ await params.bus.publishOutbound({
13731
+ channel: message.channel,
13732
+ chatId: message.chatId,
13733
+ content: `Sorry, I encountered an error: ${formatUserFacingError(error)}`,
13734
+ media: [],
13735
+ metadata: {}
13736
+ });
13737
+ }
13738
+ }
13739
+ }
13740
+ //#endregion
12938
13741
  //#region src/cli/commands/service-support/session/service-deferred-ncp-agent.ts
12939
13742
  const DEFAULT_BASE_PATH = "/api/ncp/agent";
12940
13743
  const DEFERRED_NCP_AGENT_UNAVAILABLE = "ncp agent unavailable during startup";
@@ -13020,13 +13823,13 @@ function createDeferredUiNcpAgent(basePath = DEFAULT_BASE_PATH) {
13020
13823
  }
13021
13824
  //#endregion
13022
13825
  //#region src/cli/commands/service-support/gateway/service-gateway-startup.ts
13023
- function wireSystemSessionUpdatedPublisher(params) {
13024
- params.runtimePool.setSystemSessionUpdatedHandler(({ sessionKey }) => {
13826
+ function createSystemSessionUpdatedPublisher(params) {
13827
+ return ({ sessionKey }) => {
13025
13828
  params.publishUiEvent?.({
13026
13829
  type: "session.updated",
13027
13830
  payload: { sessionKey }
13028
13831
  });
13029
- });
13832
+ };
13030
13833
  }
13031
13834
  async function startUiShell(params) {
13032
13835
  logStartupTrace("service.start_ui_shell.begin");
@@ -13067,54 +13870,90 @@ async function startUiShell(params) {
13067
13870
  };
13068
13871
  }
13069
13872
  async function startDeferredGatewayStartup(params) {
13873
+ const { uiStartup, deferredNcpSessionService, bus, sessionManager, providerManager, cronService, gatewayController, getConfig, getExtensionRegistry, resolveMessageToolHints, hydrateCapabilities, startPluginGateways, startChannels, wakeFromRestartSentinel, onNcpAgentReady, publishSessionChange } = params;
13070
13874
  logStartupTrace("service.deferred_startup.begin");
13071
- if (params.uiStartup) try {
13875
+ try {
13072
13876
  const ncpAgent = await measureStartupAsync("service.deferred_startup.create_ui_ncp_agent", async () => await createUiNcpAgent({
13073
- bus: params.bus,
13074
- providerManager: params.providerManager,
13075
- sessionManager: params.sessionManager,
13076
- cronService: params.cronService,
13077
- gatewayController: params.gatewayController,
13078
- getConfig: params.getConfig,
13079
- getExtensionRegistry: params.getExtensionRegistry,
13080
- onSessionUpdated: params.publishSessionChange,
13877
+ bus,
13878
+ providerManager,
13879
+ sessionManager,
13880
+ cronService,
13881
+ gatewayController,
13882
+ getConfig,
13883
+ getExtensionRegistry,
13884
+ onSessionUpdated: publishSessionChange,
13081
13885
  onSessionRunStatusChanged: (payload) => {
13082
- params.uiStartup?.publish({
13886
+ uiStartup?.publish({
13083
13887
  type: "session.run-status",
13084
13888
  payload
13085
13889
  });
13086
13890
  },
13087
- resolveMessageToolHints: ({ channel, accountId }) => params.resolveMessageToolHints({
13891
+ resolveMessageToolHints: ({ channel, accountId }) => resolveMessageToolHints({
13088
13892
  channel,
13089
13893
  accountId
13090
13894
  })
13091
13895
  }));
13092
- params.deferredNcpSessionService.activate(ncpAgent.sessionApi);
13093
- params.onNcpAgentReady(ncpAgent);
13094
- params.uiStartup.deferredNcpAgent.activate(ncpAgent);
13095
- console.log("✓ UI NCP agent: ready");
13896
+ deferredNcpSessionService.activate(ncpAgent.sessionApi);
13897
+ onNcpAgentReady(ncpAgent);
13898
+ if (uiStartup) {
13899
+ uiStartup.deferredNcpAgent.activate(ncpAgent);
13900
+ console.log("✓ UI NCP agent: ready");
13901
+ } else console.log("✓ Service NCP agent: ready");
13096
13902
  } catch (error) {
13097
13903
  console.error(`UI NCP agent startup failed: ${error instanceof Error ? error.message : String(error)}`);
13098
13904
  }
13099
- if (params.hydrateCapabilities) await measureStartupAsync("service.deferred_startup.hydrate_capabilities", params.hydrateCapabilities);
13100
- await measureStartupAsync("service.deferred_startup.start_plugin_gateways", params.startPluginGateways);
13101
- await measureStartupAsync("service.deferred_startup.start_channels", params.startChannels);
13102
- await measureStartupAsync("service.deferred_startup.wake_restart_sentinel", params.wakeFromRestartSentinel);
13905
+ if (hydrateCapabilities) await measureStartupAsync("service.deferred_startup.hydrate_capabilities", hydrateCapabilities);
13906
+ await measureStartupAsync("service.deferred_startup.start_plugin_gateways", startPluginGateways);
13907
+ await measureStartupAsync("service.deferred_startup.start_channels", startChannels);
13908
+ await measureStartupAsync("service.deferred_startup.wake_restart_sentinel", wakeFromRestartSentinel);
13103
13909
  console.log("✓ Deferred startup: plugin gateways and channels settled");
13104
13910
  logStartupTrace("service.deferred_startup.end");
13105
13911
  }
13106
13912
  async function runGatewayRuntimeLoop(params) {
13107
13913
  let startupTask = null;
13108
13914
  try {
13109
- const runtimePoolTask = params.runtimePool.run();
13915
+ const runtimeLoopTask = params.runRuntimeLoop();
13110
13916
  startupTask = params.startDeferredStartup();
13111
13917
  startupTask.catch(params.onDeferredStartupError);
13112
- await runtimePoolTask;
13918
+ await runtimeLoopTask;
13113
13919
  } finally {
13114
13920
  if (startupTask) await startupTask.catch(() => void 0);
13115
13921
  await params.cleanup();
13116
13922
  }
13117
13923
  }
13924
+ async function runConfiguredGatewayRuntime(params) {
13925
+ const onSystemSessionUpdated = createSystemSessionUpdatedPublisher({ publishUiEvent: params.publishUiEvent });
13926
+ logStartupTrace("service.start_gateway.runtime_loop_begin");
13927
+ await runGatewayRuntimeLoop({
13928
+ runRuntimeLoop: () => runGatewayInboundLoop({
13929
+ bus: params.gateway.bus,
13930
+ sessionManager: params.gateway.sessionManager,
13931
+ getConfig: params.getConfig,
13932
+ resolveNcpAgent: params.getLiveUiNcpAgent,
13933
+ onSystemSessionUpdated: ({ sessionKey }) => onSystemSessionUpdated({ sessionKey })
13934
+ }),
13935
+ startDeferredStartup: () => startDeferredGatewayStartup({
13936
+ uiStartup: params.uiStartup,
13937
+ deferredNcpSessionService: params.deferredNcpSessionService,
13938
+ bus: params.gateway.bus,
13939
+ sessionManager: params.gateway.sessionManager,
13940
+ providerManager: params.gateway.providerManager,
13941
+ cronService: params.gateway.cron,
13942
+ gatewayController: params.gateway.gatewayController,
13943
+ getConfig: params.getConfig,
13944
+ getExtensionRegistry: params.getExtensionRegistry,
13945
+ resolveMessageToolHints: params.resolveMessageToolHints,
13946
+ hydrateCapabilities: params.deferredStartupHooks.hydrateCapabilities,
13947
+ startPluginGateways: params.deferredStartupHooks.startPluginGateways,
13948
+ startChannels: params.deferredStartupHooks.startChannels,
13949
+ wakeFromRestartSentinel: params.deferredStartupHooks.wakeFromRestartSentinel,
13950
+ onNcpAgentReady: params.deferredStartupHooks.onNcpAgentReady,
13951
+ publishSessionChange: params.publishSessionChange
13952
+ }),
13953
+ onDeferredStartupError: params.onDeferredStartupError,
13954
+ cleanup: params.cleanup
13955
+ });
13956
+ }
13118
13957
  //#endregion
13119
13958
  //#region src/cli/commands/ncp/session/ncp-session-realtime-change.ts
13120
13959
  function toNcpSessionRealtimeEvent(change) {
@@ -13242,6 +14081,10 @@ function createDeferredUiNcpSessionService(fallbackService) {
13242
14081
  }
13243
14082
  //#endregion
13244
14083
  //#region src/cli/commands/service-support/session/service-ncp-session-realtime-bridge.ts
14084
+ function formatBackgroundTaskError(error) {
14085
+ if (error instanceof Error) return error.stack ?? error.message;
14086
+ return String(error);
14087
+ }
13245
14088
  function createLatestOnlySessionChangePublisher(publishSessionChange) {
13246
14089
  const inFlightTasks = /* @__PURE__ */ new Map();
13247
14090
  const rerunKeys = /* @__PURE__ */ new Set();
@@ -13272,7 +14115,9 @@ function createServiceNcpSessionRealtimeBridge(params) {
13272
14115
  let publishSessionChange = async (_sessionKey) => {};
13273
14116
  let scheduleSessionChange = async (_sessionKey) => {};
13274
14117
  const deferredSessionService = createDeferredUiNcpSessionService(new UiSessionService(params.sessionManager, { onSessionUpdated: (sessionKey) => {
13275
- scheduleSessionChange(sessionKey);
14118
+ scheduleSessionChange(sessionKey).catch((error) => {
14119
+ console.error(`[session-realtime] failed to publish session change for ${sessionKey}: ${formatBackgroundTaskError(error)}`);
14120
+ });
13276
14121
  } }));
13277
14122
  const publishLatestSessionChange = async (sessionKey) => {
13278
14123
  await createNcpSessionRealtimeChangePublisher({
@@ -13297,35 +14142,26 @@ function createServiceNcpSessionRealtimeBridge(params) {
13297
14142
  //#endregion
13298
14143
  //#region src/cli/commands/plugin/plugin-registry-loader.ts
13299
14144
  function createPluginLogger() {
13300
- return {
13301
- info: (message) => console.log(message),
13302
- warn: (message) => console.warn(message),
13303
- error: (message) => console.error(message),
13304
- debug: (message) => console.debug(message)
13305
- };
14145
+ return getAppLogger("plugin.registry_loader");
13306
14146
  }
13307
14147
  function withDevFirstPartyPluginPaths(config) {
13308
- const workspaceExtensionsDir = resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR);
13309
- return {
13310
- workspaceExtensionsDir,
13311
- configWithDevPluginPaths: applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir)
13312
- };
14148
+ return resolveDevPluginLoadingContext(config, resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR));
13313
14149
  }
13314
14150
  async function loadPluginRegistryProgressively(config, workspaceDir, options = {}) {
13315
- const { workspaceExtensionsDir, configWithDevPluginPaths } = withDevFirstPartyPluginPaths(config);
14151
+ const { configWithDevPluginOverrides, excludedRoots } = withDevFirstPartyPluginPaths(config);
13316
14152
  return await loadOpenClawPluginsProgressively({
13317
- config: configWithDevPluginPaths,
14153
+ config: configWithDevPluginOverrides,
13318
14154
  workspaceDir,
13319
- excludeRoots: resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir),
14155
+ excludeRoots: excludedRoots,
13320
14156
  ...buildReservedPluginLoadOptions(),
13321
14157
  onPluginProcessed: options.onPluginProcessed,
13322
14158
  logger: createPluginLogger()
13323
14159
  });
13324
14160
  }
13325
14161
  function discoverPluginRegistryStatus(config, workspaceDir) {
13326
- const { configWithDevPluginPaths } = withDevFirstPartyPluginPaths(config);
14162
+ const { configWithDevPluginOverrides } = withDevFirstPartyPluginPaths(config);
13327
14163
  return discoverPluginStatusReport({
13328
- config: configWithDevPluginPaths,
14164
+ config: configWithDevPluginOverrides,
13329
14165
  workspaceDir
13330
14166
  });
13331
14167
  }
@@ -13335,7 +14171,6 @@ function createEmptyPluginRegistry() {
13335
14171
  tools: [],
13336
14172
  channels: [],
13337
14173
  providers: [],
13338
- engines: [],
13339
14174
  ncpAgentRuntimes: [],
13340
14175
  diagnostics: [],
13341
14176
  resolvedTools: []
@@ -13554,8 +14389,6 @@ async function hydrateServiceCapabilities(params) {
13554
14389
  params.state.extensionRegistry = nextExtensionRegistry;
13555
14390
  params.state.pluginChannelBindings = nextPluginChannelBindings;
13556
14391
  params.state.pluginUiMetadata = nextPluginUiMetadata;
13557
- params.gateway.runtimePool.applyExtensionRegistry(nextExtensionRegistry);
13558
- params.gateway.runtimePool.applyRuntimeConfig(nextConfig);
13559
14392
  params.getLiveUiNcpAgent()?.applyExtensionRegistry?.(nextExtensionRegistry);
13560
14393
  if (shouldRebuildChannels) await params.gateway.reloader.rebuildChannels(nextConfig, { start: false });
13561
14394
  params.uiStartup?.publish({
@@ -13579,7 +14412,7 @@ async function hydrateServiceCapabilities(params) {
13579
14412
  //#endregion
13580
14413
  //#region src/cli/commands/service-support/plugin/service-plugin-runtime-bridge.ts
13581
14414
  function installPluginRuntimeBridge(params) {
13582
- const { runtimePool, runtimeConfigPath, getPluginChannelBindings } = params;
14415
+ const { dispatchPrompt, runtimeConfigPath, getPluginChannelBindings } = params;
13583
14416
  setPluginRuntimeBridge({
13584
14417
  loadConfig: () => toPluginConfigView$1(resolveConfigSecrets(loadConfig(), { configPath: runtimeConfigPath }), getPluginChannelBindings()),
13585
14418
  writeConfigFile: async (nextConfigView) => {
@@ -13591,7 +14424,7 @@ function installPluginRuntimeBridge(params) {
13591
14424
  if (!request) return;
13592
14425
  try {
13593
14426
  await dispatcherOptions.onReplyStart?.();
13594
- const response = await runtimePool.processDirect(request);
14427
+ const response = await dispatchPrompt(request);
13595
14428
  const replyText = typeof response === "string" ? response : String(response ?? "");
13596
14429
  if (replyText.trim()) await dispatcherOptions.deliver({ text: replyText }, { kind: "final" });
13597
14430
  } catch (error) {
@@ -13723,7 +14556,6 @@ function applyGatewayRuntimeCapabilityState(params) {
13723
14556
  params.state.pluginChannelBindings = params.next.pluginChannelBindings;
13724
14557
  }
13725
14558
  function configureGatewayPluginRuntime(params) {
13726
- params.gateway.reloader.setApplyAgentRuntimeConfig((nextConfig) => params.gateway.runtimePool.applyRuntimeConfig(nextConfig));
13727
14559
  params.gateway.reloader.setReloadPlugins(async ({ config: nextConfig, changedPaths }) => {
13728
14560
  const result = await reloadServicePlugins({
13729
14561
  nextConfig,
@@ -13746,9 +14578,7 @@ function configureGatewayPluginRuntime(params) {
13746
14578
  });
13747
14579
  params.state.pluginUiMetadata = getPluginUiMetadataFromRegistry(result.pluginRegistry);
13748
14580
  params.state.pluginGatewayHandles = result.pluginGatewayHandles;
13749
- params.gateway.runtimePool.applyExtensionRegistry(result.extensionRegistry);
13750
14581
  params.getLiveUiNcpAgent()?.applyExtensionRegistry?.(result.extensionRegistry);
13751
- params.gateway.runtimePool.applyRuntimeConfig(nextConfig);
13752
14582
  if (result.restartChannels) console.log("Config reload: plugin channel gateways restarted.");
13753
14583
  return { restartChannels: result.restartChannels };
13754
14584
  });
@@ -13756,7 +14586,12 @@ function configureGatewayPluginRuntime(params) {
13756
14586
  await params.getLiveUiNcpAgent()?.applyMcpConfig?.(nextConfig);
13757
14587
  });
13758
14588
  installPluginRuntimeBridge({
13759
- runtimePool: params.gateway.runtimePool,
14589
+ dispatchPrompt: async (request) => await dispatchPromptOverNcp({
14590
+ config: resolveConfigSecrets$2(loadConfig$2(), { configPath: params.gateway.runtimeConfigPath }),
14591
+ sessionManager: params.gateway.sessionManager,
14592
+ resolveNcpAgent: () => params.getLiveUiNcpAgent(),
14593
+ ...request
14594
+ }),
13760
14595
  runtimeConfigPath: params.gateway.runtimeConfigPath,
13761
14596
  getPluginChannelBindings: () => params.state.pluginChannelBindings
13762
14597
  });
@@ -13796,8 +14631,26 @@ function createDeferredGatewayStartupHooks(params) {
13796
14631
  };
13797
14632
  }
13798
14633
  //#endregion
14634
+ //#region src/cli/commands/service-support/gateway/service-gateway-runtime-lifecycle.ts
14635
+ function handleGatewayDeferredStartupError(params) {
14636
+ const message = params.error instanceof Error ? params.error.message : String(params.error);
14637
+ params.bootstrapStatus.markError(message);
14638
+ if (params.bootstrapStatus.getStatus().pluginHydration.state === "running") params.bootstrapStatus.markPluginHydrationError(message);
14639
+ console.error(`Deferred startup failed: ${params.error instanceof Error ? params.error.message : String(params.error)}`);
14640
+ }
14641
+ async function cleanupGatewayRuntime(params) {
14642
+ localUiRuntimeStore.clearIfOwnedByProcess();
14643
+ await params.fileWatchers.clear();
14644
+ params.resetRuntimeState();
14645
+ params.clearRealtimeBridge();
14646
+ await params.uiStartup?.deferredNcpAgent.close();
14647
+ await params.remoteModule?.stop();
14648
+ await stopPluginChannelGateways(params.runtimeState?.pluginGatewayHandles ?? []);
14649
+ setPluginRuntimeBridge(null);
14650
+ }
14651
+ //#endregion
13799
14652
  //#region src/cli/commands/service.ts
13800
- const { APP_NAME: APP_NAME$1, getApiBase, getConfigPath: getConfigPath$1, getProvider, getProviderName, getWorkspacePath: getWorkspacePath$1, LiteLLMProvider, loadConfig: loadConfig$1, MessageBus: MessageBus$1, resolveConfigSecrets: resolveConfigSecrets$1, SessionManager, parseAgentScopedSessionKey: parseAgentScopedSessionKey$1 } = NextclawCore;
14653
+ const { APP_NAME: APP_NAME$1, getApiBase, getConfigPath: getConfigPath$1, getProvider, getProviderName, getWorkspacePath: getWorkspacePath$1, LiteLLMProvider, loadConfig: loadConfig$1, MessageBus: MessageBus$1, resolveConfigSecrets: resolveConfigSecrets$1, SessionManager: SessionManager$1, parseAgentScopedSessionKey: parseAgentScopedSessionKey$1 } = NextclawCore;
13801
14654
  function createSkillsLoader(workspace) {
13802
14655
  const ctor = NextclawCore.SkillsLoader;
13803
14656
  if (!ctor) return null;
@@ -13807,10 +14660,14 @@ var ServiceCommands = class {
13807
14660
  applyLiveConfigReload = null;
13808
14661
  liveUiNcpAgent = null;
13809
14662
  fileWatchers = new ServiceFileWatcherRegistry();
14663
+ loggingRuntime = NextclawCore.getLoggingRuntime();
14664
+ serviceLogger = this.loggingRuntime.getLogger("service");
14665
+ loggingInstalled = false;
13810
14666
  constructor(deps) {
13811
14667
  this.deps = deps;
13812
14668
  }
13813
14669
  startGateway = async (options = {}) => {
14670
+ this.ensureRuntimeLoggingInstalled();
13814
14671
  logStartupTrace("service.start_gateway.begin");
13815
14672
  await this.fileWatchers.clear();
13816
14673
  this.applyLiveConfigReload = null;
@@ -13871,9 +14728,11 @@ var ServiceCommands = class {
13871
14728
  initialPluginRegistry: createEmptyPluginRegistry(),
13872
14729
  makeProvider: (config, providerOptions) => providerOptions?.allowMissing === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config),
13873
14730
  makeMissingProvider: (config) => this.makeMissingProvider(config),
13874
- requestRestart: (params) => this.deps.requestRestart(params)
14731
+ requestRestart: (params) => this.deps.requestRestart(params),
14732
+ getLiveUiNcpAgent: () => this.liveUiNcpAgent
13875
14733
  }));
13876
14734
  this.applyLiveConfigReload = gateway.applyLiveConfigReload;
14735
+ const loadGatewayConfig = () => resolveConfigSecrets$1(loadConfig$1(), { configPath: gateway.runtimeConfigPath });
13877
14736
  const gatewayRuntimeState = createGatewayRuntimeState(gateway);
13878
14737
  runtimeState = gatewayRuntimeState;
13879
14738
  uiStartup?.publish({
@@ -13889,10 +14748,6 @@ var ServiceCommands = class {
13889
14748
  state: gatewayRuntimeState,
13890
14749
  getLiveUiNcpAgent: () => this.liveUiNcpAgent
13891
14750
  });
13892
- wireSystemSessionUpdatedPublisher({
13893
- runtimePool: gateway.runtimePool,
13894
- publishUiEvent: uiStartup?.publish
13895
- });
13896
14751
  console.log("✓ Capability hydration: scheduled in background");
13897
14752
  await measureStartupAsync("service.start_gateway_support_services", async () => await startGatewayRuntimeSupport({
13898
14753
  cronJobs: gateway.cron.status().jobs,
@@ -13922,49 +14777,37 @@ var ServiceCommands = class {
13922
14777
  sessionManager: gateway.sessionManager
13923
14778
  })
13924
14779
  });
13925
- logStartupTrace("service.start_gateway.runtime_loop_begin");
13926
- await runGatewayRuntimeLoop({
13927
- runtimePool: gateway.runtimePool,
13928
- startDeferredStartup: () => startDeferredGatewayStartup({
13929
- uiStartup,
13930
- deferredNcpSessionService: ncpSessionRealtimeBridge.deferredSessionService,
13931
- bus: gateway.bus,
13932
- sessionManager: gateway.sessionManager,
13933
- providerManager: gateway.providerManager,
13934
- cronService: gateway.cron,
13935
- gatewayController: gateway.gatewayController,
13936
- getConfig: () => resolveConfigSecrets$1(loadConfig$1(), { configPath: gateway.runtimeConfigPath }),
13937
- getExtensionRegistry: () => gatewayRuntimeState.extensionRegistry,
13938
- resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
13939
- registry: gatewayRuntimeState.pluginRegistry,
13940
- channel,
13941
- cfg: resolveConfigSecrets$1(loadConfig$1(), { configPath: gateway.runtimeConfigPath }),
13942
- accountId
13943
- }),
13944
- hydrateCapabilities: deferredGatewayStartupHooks.hydrateCapabilities,
13945
- startPluginGateways: deferredGatewayStartupHooks.startPluginGateways,
13946
- startChannels: deferredGatewayStartupHooks.startChannels,
13947
- wakeFromRestartSentinel: deferredGatewayStartupHooks.wakeFromRestartSentinel,
13948
- onNcpAgentReady: deferredGatewayStartupHooks.onNcpAgentReady,
13949
- publishSessionChange: ncpSessionRealtimeBridge.publishSessionChange
14780
+ await runConfiguredGatewayRuntime({
14781
+ uiStartup,
14782
+ gateway,
14783
+ deferredNcpSessionService: ncpSessionRealtimeBridge.deferredSessionService,
14784
+ getConfig: loadGatewayConfig,
14785
+ getExtensionRegistry: () => gatewayRuntimeState.extensionRegistry,
14786
+ resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
14787
+ registry: gatewayRuntimeState.pluginRegistry,
14788
+ channel,
14789
+ cfg: loadGatewayConfig(),
14790
+ accountId
13950
14791
  }),
13951
- onDeferredStartupError: (error) => {
13952
- const message = error instanceof Error ? error.message : String(error);
13953
- bootstrapStatus.markError(message);
13954
- if (bootstrapStatus.getStatus().pluginHydration.state === "running") bootstrapStatus.markPluginHydrationError(message);
13955
- console.error(`Deferred startup failed: ${error instanceof Error ? error.message : String(error)}`);
13956
- },
13957
- cleanup: async () => {
13958
- clearOwnedServiceState();
13959
- await this.fileWatchers.clear();
13960
- this.applyLiveConfigReload = null;
13961
- this.liveUiNcpAgent = null;
13962
- ncpSessionRealtimeBridge.clear();
13963
- await uiStartup?.deferredNcpAgent.close();
13964
- await gateway.remoteModule?.stop();
13965
- await stopPluginChannelGateways(runtimeState?.pluginGatewayHandles ?? []);
13966
- setPluginRuntimeBridge(null);
13967
- }
14792
+ deferredStartupHooks: deferredGatewayStartupHooks,
14793
+ getLiveUiNcpAgent: () => this.liveUiNcpAgent,
14794
+ publishSessionChange: ncpSessionRealtimeBridge.publishSessionChange,
14795
+ publishUiEvent: uiStartup?.publish,
14796
+ onDeferredStartupError: (error) => handleGatewayDeferredStartupError({
14797
+ bootstrapStatus,
14798
+ error
14799
+ }),
14800
+ cleanup: async () => await cleanupGatewayRuntime({
14801
+ fileWatchers: this.fileWatchers,
14802
+ resetRuntimeState: () => {
14803
+ this.applyLiveConfigReload = null;
14804
+ this.liveUiNcpAgent = null;
14805
+ },
14806
+ clearRealtimeBridge: () => ncpSessionRealtimeBridge.clear(),
14807
+ uiStartup,
14808
+ remoteModule: gateway.remoteModule,
14809
+ runtimeState
14810
+ })
13968
14811
  });
13969
14812
  logStartupTrace("service.start_gateway.end");
13970
14813
  };
@@ -14068,7 +14911,7 @@ var ServiceCommands = class {
14068
14911
  if (binding.host !== uiConfig.host || binding.port !== uiConfig.port) {
14069
14912
  console.log(`Detected running service UI bind (${binding.host}:${binding.port}); enforcing (${uiConfig.host}:${uiConfig.port})...`);
14070
14913
  await this.stopService();
14071
- const stateAfterStop = readServiceState();
14914
+ const stateAfterStop = managedServiceStateStore.read();
14072
14915
  if (stateAfterStop && isProcessRunning(stateAfterStop.pid)) {
14073
14916
  process.exitCode = 1;
14074
14917
  console.error("Error: Failed to stop running service while enforcing public UI exposure.");
@@ -14083,12 +14926,14 @@ var ServiceCommands = class {
14083
14926
  return true;
14084
14927
  };
14085
14928
  startService = async (options) => {
14929
+ this.loggingRuntime.ensureReady();
14930
+ const { open, startupTimeoutMs, uiOverrides } = options;
14086
14931
  const config = loadConfig$1();
14087
- const uiConfig = resolveUiConfig(config, options.uiOverrides);
14932
+ const uiConfig = resolveUiConfig(config, uiOverrides);
14088
14933
  const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
14089
14934
  const apiUrl = `${uiUrl}/api`;
14090
14935
  const staticDir = resolveUiStaticDir();
14091
- const existing = readServiceState();
14936
+ const existing = managedServiceStateStore.read();
14092
14937
  if (existing && isProcessRunning(existing.pid)) {
14093
14938
  await this.handleExistingManagedService({
14094
14939
  existing,
@@ -14097,7 +14942,7 @@ var ServiceCommands = class {
14097
14942
  });
14098
14943
  return;
14099
14944
  }
14100
- if (existing) clearServiceState();
14945
+ if (existing) managedServiceStateStore.clear();
14101
14946
  if (!staticDir) {
14102
14947
  process.exitCode = 1, console.error(`Error: ${APP_NAME$1} UI frontend bundle not found. Reinstall or rebuild ${APP_NAME$1}. For dev-only overrides, set NEXTCLAW_UI_STATIC_DIR to a built frontend directory.`);
14103
14948
  return;
@@ -14112,6 +14957,40 @@ var ServiceCommands = class {
14112
14957
  process.exitCode = 1, console.error(`Error: Cannot start ${APP_NAME$1} because UI port ${uiConfig.port} is already occupied.`), console.error(portPreflight.message);
14113
14958
  return;
14114
14959
  }
14960
+ if (portPreflight.reusedExistingHealthyTarget) {
14961
+ await this.reuseExistingHealthyStartTarget({
14962
+ uiConfig,
14963
+ uiUrl,
14964
+ apiUrl,
14965
+ open
14966
+ });
14967
+ return;
14968
+ }
14969
+ await this.startNewManagedServiceTarget({
14970
+ config,
14971
+ uiConfig,
14972
+ uiUrl,
14973
+ apiUrl,
14974
+ healthUrl,
14975
+ startupTimeoutMs
14976
+ });
14977
+ if (open) openBrowser(uiUrl);
14978
+ };
14979
+ reuseExistingHealthyStartTarget = async (params) => {
14980
+ const { apiUrl, open, uiConfig, uiUrl } = params;
14981
+ console.log(`✓ ${APP_NAME$1} is already serving the target UI/API port`);
14982
+ console.log(`UI: ${uiUrl}`);
14983
+ console.log(`API: ${apiUrl}`);
14984
+ console.warn([
14985
+ `Warning: The healthy listener on ${uiConfig.port} is not tracked by ${managedServiceStateStore.path}.`,
14986
+ "This start call reused the existing runtime instead of spawning another one.",
14987
+ "Use the owning process or port-level tools to stop it; managed stop/restart will not control it automatically."
14988
+ ].join(" "));
14989
+ await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
14990
+ if (open) openBrowser(uiUrl);
14991
+ };
14992
+ startNewManagedServiceTarget = async (params) => {
14993
+ const { apiUrl, config, healthUrl, startupTimeoutMs, uiConfig, uiUrl } = params;
14115
14994
  const startup = spawnManagedService({
14116
14995
  appName: APP_NAME$1,
14117
14996
  config,
@@ -14119,13 +14998,14 @@ var ServiceCommands = class {
14119
14998
  uiUrl,
14120
14999
  apiUrl,
14121
15000
  healthUrl,
14122
- startupTimeoutMs: options.startupTimeoutMs,
15001
+ startupTimeoutMs,
14123
15002
  resolveStartupTimeoutMs: this.resolveStartupTimeoutMs,
14124
15003
  appendStartupStage: this.appendStartupStage,
14125
15004
  printStartupFailureDiagnostics: this.printStartupFailureDiagnostics,
14126
15005
  resolveServiceLogPath
14127
15006
  });
14128
15007
  if (!startup) {
15008
+ this.serviceLogger.fatal("managed service startup aborted", { reason: "child_process_not_created" });
14129
15009
  process.exitCode = 1;
14130
15010
  return;
14131
15011
  }
@@ -14141,22 +15021,27 @@ var ServiceCommands = class {
14141
15021
  waitForBackgroundServiceReady: this.waitForBackgroundServiceReady,
14142
15022
  isProcessRunning
14143
15023
  });
14144
- if (!readiness.ready) {
14145
- if (!isProcessRunning(startup.snapshot.pid)) {
14146
- process.exitCode = 1;
14147
- clearServiceState();
14148
- const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
14149
- this.appendStartupStage(startup.logPath, `startup failed: process exited before ready.${hint}`);
14150
- console.error(`Error: Failed to start background service. Check logs: ${startup.logPath}.${hint}`);
14151
- this.printStartupFailureDiagnostics({
14152
- uiUrl,
14153
- apiUrl,
14154
- healthUrl,
14155
- logPath: startup.logPath,
14156
- lastProbeError: readiness.lastProbeError
14157
- });
14158
- return;
14159
- }
15024
+ if (!readiness.ready && !isProcessRunning(startup.snapshot.pid)) {
15025
+ process.exitCode = 1;
15026
+ managedServiceStateStore.clear();
15027
+ const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
15028
+ this.appendStartupStage(startup.logPath, `startup failed: process exited before ready.${hint}`);
15029
+ this.serviceLogger.fatal("managed service exited before readiness completed", {
15030
+ uiUrl,
15031
+ apiUrl,
15032
+ healthUrl,
15033
+ logPath: startup.logPath,
15034
+ ...readiness.lastProbeError ? { lastProbeError: readiness.lastProbeError } : {}
15035
+ });
15036
+ console.error(`Error: Failed to start background service. Check logs: ${startup.logPath}.${hint}`);
15037
+ this.printStartupFailureDiagnostics({
15038
+ uiUrl,
15039
+ apiUrl,
15040
+ healthUrl,
15041
+ logPath: startup.logPath,
15042
+ lastProbeError: readiness.lastProbeError
15043
+ });
15044
+ return;
14160
15045
  }
14161
15046
  startup.child.unref();
14162
15047
  await reportManagedServiceStart({
@@ -14174,17 +15059,16 @@ var ServiceCommands = class {
14174
15059
  printPublicUiUrls: this.printPublicUiUrls,
14175
15060
  printServiceControlHints: this.printServiceControlHints
14176
15061
  });
14177
- if (options.open) openBrowser(uiUrl);
14178
15062
  };
14179
15063
  stopService = async () => {
14180
- const state = readServiceState();
15064
+ const state = managedServiceStateStore.read();
14181
15065
  if (!state) {
14182
- console.log("No running service found.");
15066
+ console.log("No running background service found.");
14183
15067
  return;
14184
15068
  }
14185
15069
  if (!isProcessRunning(state.pid)) {
14186
15070
  console.log("Service is not running. Cleaning up state.");
14187
- clearServiceState();
15071
+ managedServiceStateStore.clear();
14188
15072
  return;
14189
15073
  }
14190
15074
  console.log(`Stopping ${APP_NAME$1} (PID ${state.pid})...`);
@@ -14203,7 +15087,8 @@ var ServiceCommands = class {
14203
15087
  }
14204
15088
  await waitForExit(state.pid, 2e3);
14205
15089
  }
14206
- clearServiceState();
15090
+ managedServiceStateStore.clear();
15091
+ localUiRuntimeStore.clearIfOwnedByProcess(state.pid);
14207
15092
  console.log(`✓ ${APP_NAME$1} stopped`);
14208
15093
  };
14209
15094
  waitForBackgroundServiceReady = async (params) => {
@@ -14243,14 +15128,14 @@ var ServiceCommands = class {
14243
15128
  };
14244
15129
  appendStartupStage = (logPath, message) => {
14245
15130
  try {
14246
- appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [startup] ${message}\n`, "utf-8");
15131
+ this.serviceLogger.child("startup").info(message, { logPath });
14247
15132
  } catch (error) {
14248
15133
  const detail = error instanceof Error ? error.message : String(error);
14249
15134
  console.error(`Warning: failed to write startup diagnostics log (${logPath}): ${detail}`);
14250
15135
  }
14251
15136
  };
14252
15137
  printStartupFailureDiagnostics = (params) => {
14253
- const statePath = resolveServiceStatePath();
15138
+ const statePath = managedServiceStateStore.path;
14254
15139
  const lines = [
14255
15140
  "Startup diagnostics:",
14256
15141
  `- UI URL: ${params.uiUrl}`,
@@ -14264,20 +15149,22 @@ var ServiceCommands = class {
14264
15149
  };
14265
15150
  checkUiPortPreflight = async (params) => {
14266
15151
  const { healthUrl, host, port } = params;
14267
- const availability = await checkPortAvailability({
15152
+ const target = await inspectUiTarget({
14268
15153
  host,
14269
- port
14270
- });
14271
- if (availability.available) return { ok: true };
14272
- const probe = await probeHealthEndpoint(healthUrl);
14273
- const lines = [`Port probe: ${availability.detail}`];
14274
- if (probe.healthy) {
14275
- lines.push(`Health probe: ${healthUrl} is already healthy. Another process is already serving this UI/API port.`);
14276
- lines.push(`This usually means a healthy ${APP_NAME$1} instance is already serving the port, but it is not tracked by the current managed service state, so restart cannot stop it automatically.`);
14277
- } else if (probe.error) {
14278
- lines.push(`Health probe: ${probe.error}`);
14279
- lines.push("The port is occupied by a process that does not answer as a healthy NextClaw HTTP server.");
14280
- }
15154
+ port,
15155
+ healthUrl
15156
+ });
15157
+ if (target.state === "available") return {
15158
+ ok: true,
15159
+ reusedExistingHealthyTarget: false
15160
+ };
15161
+ if (target.state === "healthy-existing") return {
15162
+ ok: true,
15163
+ reusedExistingHealthyTarget: true
15164
+ };
15165
+ const lines = [`Port probe: ${target.availabilityDetail}`];
15166
+ if (target.probeError) lines.push(`Health probe: ${target.probeError}`);
15167
+ lines.push("The port is occupied by a process that does not answer as a healthy NextClaw HTTP server.");
14281
15168
  lines.push(`Fix: free port ${port} or start NextClaw with another port via --ui-port <port>.`);
14282
15169
  lines.push(`Inspect locally with: ss -ltnp | grep ${port} || lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
14283
15170
  return {
@@ -14334,6 +15221,17 @@ var ServiceCommands = class {
14334
15221
  console.log("Service controls:");
14335
15222
  console.log(` - Check status: ${APP_NAME$1} status`);
14336
15223
  console.log(` - If you need to stop the service, run: ${APP_NAME$1} stop`);
15224
+ console.log(` - View log paths: ${APP_NAME$1} logs path`);
15225
+ console.log(` - Tail recent logs: ${APP_NAME$1} logs tail`);
15226
+ };
15227
+ ensureRuntimeLoggingInstalled = () => {
15228
+ if (this.loggingInstalled) return;
15229
+ NextclawCore.configureAppLogging({
15230
+ installConsoleMirror: true,
15231
+ installProcessCrashMonitor: true
15232
+ });
15233
+ this.serviceLogger.info("runtime logging ready", { startupId: this.loggingRuntime.getStartupId() });
15234
+ this.loggingInstalled = true;
14337
15235
  };
14338
15236
  installBuiltinMarketplaceSkill = (slug, _force) => {
14339
15237
  if (!(createSkillsLoader(getWorkspacePath$1(loadConfig$1().agents.defaults.workspace))?.listSkills(false) ?? []).find((skill) => skill.name === slug && skill.source === "builtin")) return null;
@@ -14538,8 +15436,7 @@ var WorkspaceManager = class {
14538
15436
  }
14539
15437
  };
14540
15438
  //#endregion
14541
- //#region src/cli/runtime.ts
14542
- const LOGO = "🤖";
15439
+ //#region src/cli/commands/agent/cli-agent-runner.ts
14543
15440
  const EXIT_COMMANDS = new Set([
14544
15441
  "exit",
14545
15442
  "quit",
@@ -14547,6 +15444,91 @@ const EXIT_COMMANDS = new Set([
14547
15444
  "/quit",
14548
15445
  ":q"
14549
15446
  ]);
15447
+ function buildCliSharedMetadata(opts) {
15448
+ return typeof opts.model === "string" && opts.model.trim() ? { model: opts.model.trim() } : {};
15449
+ }
15450
+ function createCliHistoryInterface() {
15451
+ const historyFile = join(getDataDir(), "history", "cli_history");
15452
+ mkdirSync(resolve(historyFile, ".."), { recursive: true });
15453
+ const history = existsSync(historyFile) ? readFileSync(historyFile, "utf-8").split("\n").filter(Boolean) : [];
15454
+ const rl = createInterface({
15455
+ input: process.stdin,
15456
+ output: process.stdout
15457
+ });
15458
+ rl.on("close", () => {
15459
+ writeFileSync(historyFile, history.concat(rl.history ?? []).join("\n"));
15460
+ process.exit(0);
15461
+ });
15462
+ return rl;
15463
+ }
15464
+ async function runCliInteractiveLoop(params) {
15465
+ console.log(`${params.logo} Interactive mode (type exit or Ctrl+C to quit)\n`);
15466
+ const rl = createCliHistoryInterface();
15467
+ let running = true;
15468
+ while (running) {
15469
+ const trimmed = (await prompt(rl, "You: ")).trim();
15470
+ if (!trimmed) continue;
15471
+ if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
15472
+ rl.close();
15473
+ running = false;
15474
+ break;
15475
+ }
15476
+ printAgentResponse(await dispatchPromptOverNcp({
15477
+ config: params.config,
15478
+ sessionManager: params.sessionManager,
15479
+ resolveNcpAgent: () => params.ncpAgent,
15480
+ sessionKey: params.sessionKey,
15481
+ content: trimmed,
15482
+ metadata: params.metadata
15483
+ }));
15484
+ }
15485
+ }
15486
+ async function runCliAgentCommand(params) {
15487
+ const bus = new MessageBus();
15488
+ const sessionManager = new SessionManager({
15489
+ workspace: params.workspace,
15490
+ homeDir: getDataDir()
15491
+ });
15492
+ const ncpAgent = await createUiNcpAgent({
15493
+ bus,
15494
+ providerManager: params.providerManager,
15495
+ sessionManager,
15496
+ getConfig: params.loadResolvedConfig,
15497
+ getExtensionRegistry: () => params.extensionRegistry,
15498
+ resolveMessageToolHints: ({ channel, accountId }) => params.resolveMessageToolHints({
15499
+ channel,
15500
+ accountId
15501
+ })
15502
+ });
15503
+ try {
15504
+ const sessionKey = params.opts.session ?? "cli:default";
15505
+ const sharedMetadata = buildCliSharedMetadata(params.opts);
15506
+ if (params.opts.message) {
15507
+ printAgentResponse(await dispatchPromptOverNcp({
15508
+ config: params.config,
15509
+ sessionManager,
15510
+ resolveNcpAgent: () => ncpAgent,
15511
+ sessionKey,
15512
+ content: params.opts.message,
15513
+ metadata: sharedMetadata
15514
+ }));
15515
+ return;
15516
+ }
15517
+ await runCliInteractiveLoop({
15518
+ logo: params.logo,
15519
+ config: params.config,
15520
+ sessionManager,
15521
+ ncpAgent,
15522
+ sessionKey,
15523
+ metadata: sharedMetadata
15524
+ });
15525
+ } finally {
15526
+ await ncpAgent.dispose?.();
15527
+ }
15528
+ }
15529
+ //#endregion
15530
+ //#region src/cli/runtime.ts
15531
+ const LOGO = "🤖";
14550
15532
  const FORCED_PUBLIC_UI_HOST = "0.0.0.0";
14551
15533
  var CliRuntime = class {
14552
15534
  logo;
@@ -14566,6 +15548,7 @@ var CliRuntime = class {
14566
15548
  remoteCommands;
14567
15549
  remote;
14568
15550
  diagnosticsCommands;
15551
+ logsCommands;
14569
15552
  constructor(options = {}) {
14570
15553
  logStartupTrace("cli.runtime.constructor.begin");
14571
15554
  this.logo = options.logo ?? "🤖";
@@ -14598,8 +15581,9 @@ var CliRuntime = class {
14598
15581
  hasRunningManagedService: hasRunningNextclawManagedService
14599
15582
  }));
14600
15583
  this.diagnosticsCommands = measureStartupSync("cli.runtime.diagnostics_commands", () => new DiagnosticsCommands({ logo: this.logo }));
15584
+ this.logsCommands = measureStartupSync("cli.runtime.logs_commands", () => new LogsCommands());
14601
15585
  this.restartCoordinator = measureStartupSync("cli.runtime.restart_coordinator", () => new RestartCoordinator({
14602
- readServiceState,
15586
+ readServiceState: managedServiceStateStore.read,
14603
15587
  isProcessRunning,
14604
15588
  currentPid: () => process.pid,
14605
15589
  restartBackgroundService: async (reason) => this.restartBackgroundService(reason),
@@ -14619,7 +15603,7 @@ var CliRuntime = class {
14619
15603
  restartBackgroundService = async (reason) => {
14620
15604
  if (this.serviceRestartTask) return this.serviceRestartTask;
14621
15605
  this.serviceRestartTask = (async () => {
14622
- const state = readServiceState();
15606
+ const state = managedServiceStateStore.read();
14623
15607
  if (!state || !isProcessRunning(state.pid) || state.pid === process.pid) return false;
14624
15608
  const uiHost = FORCED_PUBLIC_UI_HOST;
14625
15609
  const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 55667;
@@ -14645,7 +15629,7 @@ var CliRuntime = class {
14645
15629
  const strategy = params.strategy ?? "background-service-or-manual";
14646
15630
  if (strategy !== "background-service-or-exit" && strategy !== "exit-process") return;
14647
15631
  if (this.selfRelaunchArmed) return;
14648
- const state = readServiceState();
15632
+ const state = managedServiceStateStore.read();
14649
15633
  if (!state || state.pid !== process.pid) return;
14650
15634
  const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 55667;
14651
15635
  const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
@@ -14655,7 +15639,7 @@ var CliRuntime = class {
14655
15639
  "--ui-port",
14656
15640
  String(uiPort)
14657
15641
  ];
14658
- const serviceStatePath = resolve(getDataDir(), "run", "service.json");
15642
+ const serviceStatePath = managedServiceStateStore.path;
14659
15643
  const helperScript = [
14660
15644
  "const { spawnSync } = require(\"node:child_process\");",
14661
15645
  "const { readFileSync } = require(\"node:fs\");",
@@ -14840,13 +15824,13 @@ var CliRuntime = class {
14840
15824
  uiPort: opts.uiPort,
14841
15825
  forcedPublicHost: FORCED_PUBLIC_UI_HOST
14842
15826
  });
14843
- const state = readServiceState();
15827
+ const state = managedServiceStateStore.read();
14844
15828
  if (state && isProcessRunning(state.pid)) {
14845
15829
  console.log(`Restarting ${APP_NAME}...`);
14846
15830
  await this.serviceCommands.stopService();
14847
15831
  } else {
14848
15832
  if (state) {
14849
- clearServiceState();
15833
+ managedServiceStateStore.clear();
14850
15834
  console.log("Service state was stale and has been cleaned up.");
14851
15835
  }
14852
15836
  const unmanagedHealthyServiceMessage = await describeUnmanagedHealthyTargetMessage({ uiOverrides });
@@ -14888,22 +15872,19 @@ var CliRuntime = class {
14888
15872
  }
14889
15873
  });
14890
15874
  try {
14891
- const agentLoop = new AgentLoop({
14892
- bus: new MessageBus(),
14893
- providerManager: new ProviderManager({
14894
- defaultProvider: this.serviceCommands.createProvider(config) ?? this.serviceCommands.createMissingProvider(config),
14895
- config
14896
- }),
14897
- workspace,
14898
- model: config.agents.defaults.model,
14899
- maxIterations: config.agents.defaults.maxToolIterations,
14900
- contextTokens: config.agents.defaults.contextTokens,
14901
- searchConfig: config.search,
14902
- execConfig: config.tools.exec,
14903
- restrictToWorkspace: config.tools.restrictToWorkspace,
14904
- contextConfig: config.agents.context,
15875
+ const provider = this.serviceCommands.createProvider(config) ?? this.serviceCommands.createMissingProvider(config);
15876
+ const providerManager = this.createObservedProviderManager(new ProviderManager({
15877
+ defaultProvider: provider,
15878
+ config
15879
+ }), "cli-agent");
15880
+ await runCliAgentCommand({
15881
+ logo: this.logo,
15882
+ opts,
14905
15883
  config,
15884
+ workspace,
15885
+ providerManager,
14906
15886
  extensionRegistry,
15887
+ loadResolvedConfig: () => resolveConfigSecrets(loadConfig(), { configPath }),
14907
15888
  resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
14908
15889
  registry: pluginRegistry,
14909
15890
  channel,
@@ -14911,43 +15892,6 @@ var CliRuntime = class {
14911
15892
  accountId
14912
15893
  })
14913
15894
  });
14914
- if (opts.message) {
14915
- printAgentResponse(await agentLoop.processDirect({
14916
- content: opts.message,
14917
- sessionKey: opts.session ?? "cli:default",
14918
- channel: "cli",
14919
- chatId: "direct",
14920
- metadata: typeof opts.model === "string" && opts.model.trim() ? { model: opts.model.trim() } : {}
14921
- }));
14922
- return;
14923
- }
14924
- console.log(`${this.logo} Interactive mode (type exit or Ctrl+C to quit)\n`);
14925
- const historyFile = join(getDataDir(), "history", "cli_history");
14926
- mkdirSync(resolve(historyFile, ".."), { recursive: true });
14927
- const history = existsSync(historyFile) ? readFileSync(historyFile, "utf-8").split("\n").filter(Boolean) : [];
14928
- const rl = createInterface({
14929
- input: process.stdin,
14930
- output: process.stdout
14931
- });
14932
- rl.on("close", () => {
14933
- writeFileSync(historyFile, history.concat(rl.history ?? []).join("\n"));
14934
- process.exit(0);
14935
- });
14936
- let running = true;
14937
- while (running) {
14938
- const trimmed = (await prompt(rl, "You: ")).trim();
14939
- if (!trimmed) continue;
14940
- if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
14941
- rl.close();
14942
- running = false;
14943
- break;
14944
- }
14945
- printAgentResponse(await agentLoop.processDirect({
14946
- content: trimmed,
14947
- sessionKey: opts.session ?? "cli:default",
14948
- metadata: typeof opts.model === "string" && opts.model.trim() ? { model: opts.model.trim() } : {}
14949
- }));
14950
- }
14951
15895
  } finally {
14952
15896
  setPluginRuntimeBridge(null);
14953
15897
  }
@@ -14964,28 +15908,19 @@ var CliRuntime = class {
14964
15908
  }
14965
15909
  const versionBefore = getPackageVersion$1();
14966
15910
  console.log(`Current version: ${versionBefore}`);
14967
- const result = runSelfUpdate({
14968
- timeoutMs,
14969
- cwd: process.cwd()
15911
+ const report = reportSelfUpdateResult({
15912
+ appName: APP_NAME,
15913
+ currentVersion: versionBefore,
15914
+ result: runSelfUpdate({
15915
+ timeoutMs,
15916
+ cwd: process.cwd(),
15917
+ currentVersion: versionBefore
15918
+ }),
15919
+ readInstalledVersion: getPackageVersion$1
14970
15920
  });
14971
- const printSteps = () => {
14972
- for (const step of result.steps) {
14973
- console.log(`- ${step.cmd} ${step.args.join(" ")} (code ${step.code ?? "?"})`);
14974
- if (step.stderr) console.log(` stderr: ${step.stderr}`);
14975
- if (step.stdout) console.log(` stdout: ${step.stdout}`);
14976
- }
14977
- };
14978
- if (!result.ok) {
14979
- console.error(`Update failed: ${result.error ?? "unknown error"}`);
14980
- if (result.steps.length > 0) printSteps();
14981
- process.exit(1);
14982
- }
14983
- const versionAfter = getPackageVersion$1();
14984
- console.log(`✓ Update complete (${result.strategy})`);
14985
- if (versionAfter === versionBefore) console.log(`Version unchanged: ${versionBefore}`);
14986
- else console.log(`Version updated: ${versionBefore} -> ${versionAfter}`);
14987
- const state = readServiceState();
14988
- if (state && isProcessRunning(state.pid)) console.log(`Tip: restart ${APP_NAME} to apply the update.`);
15921
+ if (!report.ok) process.exit(1);
15922
+ const state = managedServiceStateStore.read();
15923
+ if (report.shouldSuggestRestart && state && isProcessRunning(state.pid)) console.log(`Tip: restart ${APP_NAME} to apply the update.`);
14989
15924
  };
14990
15925
  agentsList = (opts = {}) => {
14991
15926
  this.agentCommands.agentsList(opts);
@@ -15092,6 +16027,12 @@ var CliRuntime = class {
15092
16027
  doctor = async (opts = {}) => {
15093
16028
  await this.diagnosticsCommands.doctor(opts);
15094
16029
  };
16030
+ logsPath = () => {
16031
+ this.logsCommands.logsPath();
16032
+ };
16033
+ logsTail = (opts = {}) => {
16034
+ this.logsCommands.logsTail(opts);
16035
+ };
15095
16036
  skillsInstall = async (options) => {
15096
16037
  const config = loadConfig();
15097
16038
  const workdir = resolveSkillsInstallWorkdir({
@@ -15119,6 +16060,7 @@ var CliRuntime = class {
15119
16060
  console.log(` Alias: ${result.slug}`);
15120
16061
  console.log(` Files: ${result.fileCount}`);
15121
16062
  };
16063
+ createObservedProviderManager = (providerManager, source) => new ObservedProviderManager(providerManager, new LlmUsageObserver(llmUsageRecorder, source));
15122
16064
  };
15123
16065
  //#endregion
15124
16066
  //#region src/cli/register-agents-commands.ts
@@ -15135,6 +16077,7 @@ function registerAgentsCommands(program, runtime) {
15135
16077
  logStartupTrace("cli.index.module_loaded");
15136
16078
  const program = new Command();
15137
16079
  const runtime = measureStartupSync("cli.runtime.construct", () => new CliRuntime({ logo: LOGO }));
16080
+ const llmUsageCommands = new LlmUsageCommands();
15138
16081
  program.name(APP_NAME).description(`${LOGO} ${APP_NAME} - ${APP_TAGLINE}`).version(getPackageVersion$1(), "-v, --version", "show version");
15139
16082
  program.command("onboard").description(`Initialize ${APP_NAME} configuration and workspace`).action(async () => runtime.onboard());
15140
16083
  program.command("init").description(`Initialize ${APP_NAME} configuration and workspace`).option("-f, --force", "Overwrite existing template files").action(async (opts) => runtime.init({ force: Boolean(opts.force) }));
@@ -15203,6 +16146,10 @@ cron.command("disable <jobId>").action(async (jobId) => runtime.cronEnable(jobId
15203
16146
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
15204
16147
  program.command("status").description(`Show ${APP_NAME} 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));
15205
16148
  program.command("doctor").description(`Run ${APP_NAME} 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));
16149
+ const logs = program.command("logs").description("Inspect local runtime logs");
16150
+ logs.command("path").description("Show local log file paths").action(() => runtime.logsPath());
16151
+ logs.command("tail").description("Show recent local log entries").option("--lines <n>", "Number of lines to show", "40").option("--crash", "Tail crash.log instead of service.log", false).action((opts) => runtime.logsTail(opts));
16152
+ program.command("usage").description("Show observed LLM usage snapshots, history, and prompt cache stats").option("--history", "Show recent usage history", false).option("--stats", "Show aggregated usage stats from local history", false).option("--limit <n>", "Maximum number of history records to show", "10").option("--json", "Output JSON", false).action(async (opts) => llmUsageCommands.show(opts));
15206
16153
  program.parseAsync(process.argv);
15207
16154
  //#endregion
15208
16155
  export {};