nextclaw 0.17.6 → 0.17.8

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 +1833 -943
  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
@@ -6020,6 +6288,40 @@ function runSelfUpdate(options = {}) {
6020
6288
  };
6021
6289
  }
6022
6290
  //#endregion
6291
+ //#region src/cli/runtime-state/managed-service-state.store.ts
6292
+ var ManagedServiceStateStore = class {
6293
+ get path() {
6294
+ return resolve(getDataDir(), "run", "service.json");
6295
+ }
6296
+ read = () => {
6297
+ if (!existsSync(this.path)) return null;
6298
+ try {
6299
+ const raw = readFileSync(this.path, "utf-8");
6300
+ return JSON.parse(raw);
6301
+ } catch {
6302
+ return null;
6303
+ }
6304
+ };
6305
+ write = (state) => {
6306
+ mkdirSync(resolve(this.path, ".."), { recursive: true });
6307
+ writeFileSync(this.path, JSON.stringify(state, null, 2));
6308
+ };
6309
+ update = (updater) => {
6310
+ const current = this.read();
6311
+ if (!current) return null;
6312
+ const next = updater(current);
6313
+ this.write(next);
6314
+ return next;
6315
+ };
6316
+ clear = () => {
6317
+ if (existsSync(this.path)) rmSync(this.path, { force: true });
6318
+ };
6319
+ clearIfOwnedByProcess = (pid = process.pid) => {
6320
+ if (this.read()?.pid === pid) this.clear();
6321
+ };
6322
+ };
6323
+ const managedServiceStateStore = new ManagedServiceStateStore();
6324
+ //#endregion
6023
6325
  //#region src/cli/commands/plugin/plugin-command-utils.ts
6024
6326
  const RESERVED_PROVIDER_IDS$1 = builtinProviderIds();
6025
6327
  const RESERVED_TOOL_NAMES = [
@@ -6045,7 +6347,6 @@ function buildReservedPluginLoadOptions() {
6045
6347
  reservedToolNames: [...RESERVED_TOOL_NAMES],
6046
6348
  reservedChannelIds: [],
6047
6349
  reservedProviderIds: RESERVED_PROVIDER_IDS$1,
6048
- reservedEngineKinds: ["native"],
6049
6350
  reservedNcpAgentRuntimeKinds: ["native"]
6050
6351
  };
6051
6352
  }
@@ -6053,11 +6354,10 @@ function appendPluginCapabilityLines(lines, plugin) {
6053
6354
  if (plugin.toolNames.length > 0) lines.push(`Tools: ${plugin.toolNames.join(", ")}`);
6054
6355
  if (plugin.channelIds.length > 0) lines.push(`Channels: ${plugin.channelIds.join(", ")}`);
6055
6356
  if (plugin.providerIds.length > 0) lines.push(`Providers: ${plugin.providerIds.join(", ")}`);
6056
- if (plugin.engineKinds.length > 0) lines.push(`Engines: ${plugin.engineKinds.join(", ")}`);
6057
6357
  if (plugin.ncpAgentRuntimeKinds.length > 0) lines.push(`NCP runtimes: ${plugin.ncpAgentRuntimeKinds.join(", ")}`);
6058
6358
  }
6059
6359
  //#endregion
6060
- //#region src/cli/commands/plugin/dev-first-party-plugin-load-paths.ts
6360
+ //#region src/cli/commands/plugin/development-source/first-party-plugin-load-paths.ts
6061
6361
  const readJsonFile = (filePath) => {
6062
6362
  try {
6063
6363
  const raw = fs.readFileSync(filePath, "utf-8");
@@ -6074,7 +6374,7 @@ const readString = (value) => {
6074
6374
  const resolveDevFirstPartyPluginDir = (explicitDir, moduleDir = path.dirname(fileURLToPath(import.meta.url))) => {
6075
6375
  const configured = explicitDir?.trim();
6076
6376
  if (configured) return configured;
6077
- const inferred = path.resolve(moduleDir, "../../../../extensions");
6377
+ const inferred = path.resolve(moduleDir, "../../../../../extensions");
6078
6378
  return fs.existsSync(inferred) ? inferred : void 0;
6079
6379
  };
6080
6380
  const hasOpenClawExtensions = (pkg) => {
@@ -6083,6 +6383,14 @@ const hasOpenClawExtensions = (pkg) => {
6083
6383
  const extensions = openclaw.extensions;
6084
6384
  return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.trim().length > 0);
6085
6385
  };
6386
+ const hasOpenClawDevelopmentExtensions = (pkg) => {
6387
+ const openclaw = pkg.openclaw;
6388
+ if (!openclaw || typeof openclaw !== "object" || Array.isArray(openclaw)) return false;
6389
+ const development = openclaw.development;
6390
+ if (!development || typeof development !== "object" || Array.isArray(development)) return false;
6391
+ const extensions = development.extensions;
6392
+ return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string" && entry.trim().length > 0);
6393
+ };
6086
6394
  const normalizePackageSpec = (spec) => {
6087
6395
  const trimmed = spec.trim();
6088
6396
  if (!trimmed) return;
@@ -6108,11 +6416,38 @@ const readWorkspacePluginPackages = (workspaceExtensionsDir) => {
6108
6416
  if (!packageName?.startsWith("@nextclaw/")) continue;
6109
6417
  packages.push({
6110
6418
  packageName,
6111
- dir: packageDir
6419
+ dir: packageDir,
6420
+ supportsDevelopmentSource: hasOpenClawDevelopmentExtensions(pkg)
6112
6421
  });
6113
6422
  }
6114
6423
  return packages;
6115
6424
  };
6425
+ const mergeLoadPaths$1 = (existingLoadPaths, devLoadPaths) => {
6426
+ const mergedLoadPaths = [...devLoadPaths];
6427
+ for (const entry of existingLoadPaths) if (!mergedLoadPaths.includes(entry)) mergedLoadPaths.push(entry);
6428
+ return mergedLoadPaths;
6429
+ };
6430
+ const buildDevelopmentSourceEntryDefaults = (config, workspacePackages) => {
6431
+ const packageByName = new Map(workspacePackages.map((entry) => [entry.packageName, entry]));
6432
+ const nextEntries = { ...config.plugins.entries ?? {} };
6433
+ let didDefaultDevelopmentSource = false;
6434
+ for (const [pluginId, installRecord] of Object.entries(config.plugins.installs ?? {})) {
6435
+ const packageName = normalizePackageSpec(installRecord.spec ?? "");
6436
+ if (!packageName) continue;
6437
+ if (!packageByName.get(packageName)?.supportsDevelopmentSource) continue;
6438
+ const existingEntry = nextEntries[pluginId];
6439
+ if (existingEntry?.source) continue;
6440
+ nextEntries[pluginId] = {
6441
+ ...existingEntry,
6442
+ source: "development"
6443
+ };
6444
+ didDefaultDevelopmentSource = true;
6445
+ }
6446
+ return {
6447
+ didDefaultDevelopmentSource,
6448
+ nextEntries
6449
+ };
6450
+ };
6116
6451
  const resolveDevFirstPartyPluginLoadPaths = (config, workspaceExtensionsDir) => {
6117
6452
  const rootDir = resolveDevFirstPartyPluginDir(workspaceExtensionsDir);
6118
6453
  if (!rootDir) return [];
@@ -6147,15 +6482,19 @@ const resolveDevFirstPartyPluginInstallRoots = (config, workspaceExtensionsDir)
6147
6482
  return installRoots;
6148
6483
  };
6149
6484
  const applyDevFirstPartyPluginLoadPaths = (config, workspaceExtensionsDir) => {
6150
- const devLoadPaths = resolveDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir);
6485
+ const rootDir = resolveDevFirstPartyPluginDir(workspaceExtensionsDir);
6486
+ if (!rootDir) return config;
6487
+ const workspacePackages = readWorkspacePluginPackages(rootDir);
6488
+ if (workspacePackages.length === 0) return config;
6489
+ const devLoadPaths = resolveDevFirstPartyPluginLoadPaths(config, rootDir);
6151
6490
  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);
6491
+ const mergedLoadPaths = mergeLoadPaths$1(Array.isArray(config.plugins.load?.paths) ? config.plugins.load.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [], devLoadPaths);
6492
+ const { didDefaultDevelopmentSource, nextEntries } = buildDevelopmentSourceEntryDefaults(config, workspacePackages);
6155
6493
  return {
6156
6494
  ...config,
6157
6495
  plugins: {
6158
6496
  ...config.plugins,
6497
+ entries: didDefaultDevelopmentSource ? nextEntries : config.plugins.entries,
6159
6498
  load: {
6160
6499
  ...config.plugins.load,
6161
6500
  paths: mergedLoadPaths
@@ -6164,6 +6503,112 @@ const applyDevFirstPartyPluginLoadPaths = (config, workspaceExtensionsDir) => {
6164
6503
  };
6165
6504
  };
6166
6505
  //#endregion
6506
+ //#region src/cli/commands/plugin/development-source/dev-plugin-overrides.utils.ts
6507
+ const DEV_PLUGIN_OVERRIDES_ENV = "NEXTCLAW_DEV_PLUGIN_OVERRIDES";
6508
+ function isRecord$3(value) {
6509
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
6510
+ }
6511
+ function readOptionalString$6(value) {
6512
+ if (typeof value !== "string") return;
6513
+ return value.trim() || void 0;
6514
+ }
6515
+ function readPackageManifest(pluginPath) {
6516
+ const packageJsonPath = path.join(pluginPath, "package.json");
6517
+ if (!fs.existsSync(packageJsonPath)) return null;
6518
+ try {
6519
+ return JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
6520
+ } catch {
6521
+ return null;
6522
+ }
6523
+ }
6524
+ function assertOverridePluginReadable(override) {
6525
+ if (!fs.existsSync(override.pluginPath)) throw new Error(`[dev-plugin-override] plugin path does not exist for "${override.pluginId}": ${override.pluginPath}`);
6526
+ const packageManifest = readPackageManifest(override.pluginPath);
6527
+ if (!packageManifest) throw new Error(`[dev-plugin-override] package.json is missing or invalid for "${override.pluginId}": ${override.pluginPath}`);
6528
+ const pluginManifest = loadPluginManifest(override.pluginPath);
6529
+ if (!pluginManifest.ok) throw new Error(`[dev-plugin-override] ${pluginManifest.error} for "${override.pluginId}": ${override.pluginPath}`);
6530
+ 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}`);
6531
+ if (getPackageManifestExtensions(packageManifest, override.source).length === 0) {
6532
+ const missingEntry = override.source === "development" ? "openclaw.development.extensions" : "openclaw.extensions";
6533
+ throw new Error(`[dev-plugin-override] ${missingEntry} is missing for "${override.pluginId}" at ${override.pluginPath}`);
6534
+ }
6535
+ }
6536
+ function readOverrideRecord(value, index) {
6537
+ if (!isRecord$3(value)) throw new Error(`[dev-plugin-override] override[${index}] must be an object`);
6538
+ const pluginId = readOptionalString$6(value.pluginId);
6539
+ const pluginPath = readOptionalString$6(value.pluginPath);
6540
+ const source = value.source === "development" ? "development" : "production";
6541
+ if (!pluginId || !pluginPath) throw new Error(`[dev-plugin-override] override[${index}] requires pluginId and pluginPath`);
6542
+ const normalized = {
6543
+ pluginId,
6544
+ pluginPath: path.resolve(pluginPath),
6545
+ source
6546
+ };
6547
+ assertOverridePluginReadable(normalized);
6548
+ return normalized;
6549
+ }
6550
+ function resolveDevPluginOverrides(rawEnv = process.env[DEV_PLUGIN_OVERRIDES_ENV]) {
6551
+ if (typeof rawEnv !== "string" || rawEnv.trim().length === 0) return [];
6552
+ let parsed;
6553
+ try {
6554
+ parsed = JSON.parse(rawEnv);
6555
+ } catch (error) {
6556
+ throw new Error(`[dev-plugin-override] failed to parse ${DEV_PLUGIN_OVERRIDES_ENV}: ${error instanceof Error ? error.message : String(error)}`);
6557
+ }
6558
+ if (!Array.isArray(parsed)) throw new Error(`[dev-plugin-override] ${DEV_PLUGIN_OVERRIDES_ENV} must be a JSON array`);
6559
+ const seenPluginIds = /* @__PURE__ */ new Set();
6560
+ const overrides = parsed.map((entry, index) => readOverrideRecord(entry, index));
6561
+ for (const entry of overrides) {
6562
+ if (seenPluginIds.has(entry.pluginId)) throw new Error(`[dev-plugin-override] duplicate plugin override for "${entry.pluginId}"`);
6563
+ seenPluginIds.add(entry.pluginId);
6564
+ }
6565
+ return overrides;
6566
+ }
6567
+ function mergeLoadPaths(existingLoadPaths, overrideLoadPaths) {
6568
+ const merged = [...overrideLoadPaths];
6569
+ for (const entry of existingLoadPaths) if (!merged.includes(entry)) merged.push(entry);
6570
+ return merged;
6571
+ }
6572
+ function applyExplicitDevPluginOverrides(config, overrides) {
6573
+ if (overrides.length === 0) return config;
6574
+ const nextEntries = { ...config.plugins.entries ?? {} };
6575
+ for (const override of overrides) nextEntries[override.pluginId] = {
6576
+ ...nextEntries[override.pluginId] ?? {},
6577
+ source: override.source
6578
+ };
6579
+ const existingLoadPaths = Array.isArray(config.plugins.load?.paths) ? config.plugins.load.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0) : [];
6580
+ return {
6581
+ ...config,
6582
+ plugins: {
6583
+ ...config.plugins,
6584
+ entries: nextEntries,
6585
+ load: {
6586
+ ...config.plugins.load,
6587
+ paths: mergeLoadPaths(existingLoadPaths, overrides.map((entry) => entry.pluginPath))
6588
+ }
6589
+ }
6590
+ };
6591
+ }
6592
+ function resolveDevPluginOverrideInstallRoots(config, overrides) {
6593
+ const installRoots = [];
6594
+ for (const override of overrides) {
6595
+ const installRecord = config.plugins.installs?.[override.pluginId];
6596
+ const installPath = readOptionalString$6(installRecord?.installPath);
6597
+ if (!installPath || installRoots.includes(installPath)) continue;
6598
+ installRoots.push(installPath);
6599
+ }
6600
+ return installRoots;
6601
+ }
6602
+ function resolveDevPluginLoadingContext(config, workspaceExtensionsDir, rawOverridesEnv = process.env[DEV_PLUGIN_OVERRIDES_ENV]) {
6603
+ const configWithFirstPartyOverrides = applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir);
6604
+ const overrides = resolveDevPluginOverrides(rawOverridesEnv);
6605
+ return {
6606
+ configWithDevPluginOverrides: applyExplicitDevPluginOverrides(configWithFirstPartyOverrides, overrides),
6607
+ excludedRoots: [...resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir), ...resolveDevPluginOverrideInstallRoots(config, overrides)].filter((entry, index, list) => list.indexOf(entry) === index),
6608
+ overrides
6609
+ };
6610
+ }
6611
+ //#endregion
6167
6612
  //#region src/cli/commands/plugin/plugin-mutation-actions.ts
6168
6613
  const pluginInstallLogger = {
6169
6614
  info: (message) => console.log(message),
@@ -6335,12 +6780,6 @@ function toExtensionRegistry(pluginRegistry) {
6335
6780
  channel: channel.channel,
6336
6781
  source: channel.source
6337
6782
  })),
6338
- engines: pluginRegistry.engines.map((engine) => ({
6339
- extensionId: engine.pluginId,
6340
- kind: engine.kind,
6341
- factory: engine.factory,
6342
- source: engine.source
6343
- })),
6344
6783
  ncpAgentRuntimes: pluginRegistry.ncpAgentRuntimes.map((runtime) => ({
6345
6784
  pluginId: runtime.pluginId,
6346
6785
  kind: runtime.kind,
@@ -6360,11 +6799,11 @@ function toExtensionRegistry(pluginRegistry) {
6360
6799
  //#endregion
6361
6800
  //#region src/cli/commands/plugins.ts
6362
6801
  function loadPluginRegistry(config, workspaceDir) {
6363
- const workspaceExtensionsDir = resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR);
6802
+ const { configWithDevPluginOverrides, excludedRoots } = resolveDevPluginLoadingContext(config, resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR));
6364
6803
  return loadOpenClawPlugins({
6365
- config: applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir),
6804
+ config: configWithDevPluginOverrides,
6366
6805
  workspaceDir,
6367
- excludeRoots: resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir),
6806
+ excludeRoots: excludedRoots,
6368
6807
  ...buildReservedPluginLoadOptions(),
6369
6808
  logger: {
6370
6809
  info: (message) => console.log(message),
@@ -6894,7 +7333,7 @@ var ConfigCommands = class {
6894
7333
  };
6895
7334
  //#endregion
6896
7335
  //#region src/cli/commands/mcp.ts
6897
- function normalizeOptionalString$5(value) {
7336
+ function normalizeOptionalString$7(value) {
6898
7337
  if (typeof value !== "string") return;
6899
7338
  return value.trim() || void 0;
6900
7339
  }
@@ -6917,9 +7356,9 @@ function parseTimeoutMs$1(value) {
6917
7356
  return Math.trunc(parsed);
6918
7357
  }
6919
7358
  function buildMcpServerDefinition(command, opts) {
6920
- const transport = (normalizeOptionalString$5(opts.transport) ?? "stdio").toLowerCase();
7359
+ const transport = (normalizeOptionalString$7(opts.transport) ?? "stdio").toLowerCase();
6921
7360
  const disabled = Boolean(opts.disabled);
6922
- const explicitAgents = Array.from(new Set((opts.agent ?? []).map((agentId) => normalizeOptionalString$5(agentId)).filter((agentId) => Boolean(agentId))));
7361
+ const explicitAgents = Array.from(new Set((opts.agent ?? []).map((agentId) => normalizeOptionalString$7(agentId)).filter((agentId) => Boolean(agentId))));
6923
7362
  const allAgents = explicitAgents.length === 0 ? true : Boolean(opts.allAgents);
6924
7363
  if (transport === "stdio") {
6925
7364
  if (command.length === 0) throw new Error("stdio transport requires a command after --");
@@ -6929,9 +7368,9 @@ function buildMcpServerDefinition(command, opts) {
6929
7368
  type: "stdio",
6930
7369
  command: command[0],
6931
7370
  args: command.slice(1),
6932
- cwd: normalizeOptionalString$5(opts.cwd),
7371
+ cwd: normalizeOptionalString$7(opts.cwd),
6933
7372
  env: parsePairs(opts.env, "env"),
6934
- stderr: normalizeOptionalString$5(opts.stderr) ?? "pipe"
7373
+ stderr: normalizeOptionalString$7(opts.stderr) ?? "pipe"
6935
7374
  },
6936
7375
  scope: {
6937
7376
  allAgents,
@@ -6947,7 +7386,7 @@ function buildMcpServerDefinition(command, opts) {
6947
7386
  }
6948
7387
  };
6949
7388
  }
6950
- const url = normalizeOptionalString$5(opts.url);
7389
+ const url = normalizeOptionalString$7(opts.url);
6951
7390
  if (!url) throw new Error(`${transport} transport requires --url`);
6952
7391
  const timeoutMs = parseTimeoutMs$1(opts.timeoutMs);
6953
7392
  const shared = {
@@ -7094,7 +7533,7 @@ function normalizeSecretSource(value) {
7094
7533
  const normalized = value.trim().toLowerCase();
7095
7534
  return SECRET_SOURCES.includes(normalized) ? normalized : null;
7096
7535
  }
7097
- function normalizeOptionalString$4(value) {
7536
+ function normalizeOptionalString$6(value) {
7098
7537
  if (typeof value !== "string") return;
7099
7538
  return value.trim() || void 0;
7100
7539
  }
@@ -7105,9 +7544,9 @@ function parseTimeoutMs(value) {
7105
7544
  return Math.trunc(parsed);
7106
7545
  }
7107
7546
  function inferProviderAlias(config, ref) {
7108
- const explicit = normalizeOptionalString$4(ref.provider);
7547
+ const explicit = normalizeOptionalString$6(ref.provider);
7109
7548
  if (explicit) return explicit;
7110
- const defaultAlias = normalizeOptionalString$4(config.secrets.defaults[ref.source]);
7549
+ const defaultAlias = normalizeOptionalString$6(config.secrets.defaults[ref.source]);
7111
7550
  if (defaultAlias) return defaultAlias;
7112
7551
  return ref.source;
7113
7552
  }
@@ -7125,8 +7564,8 @@ function parseRefsPatch(raw) {
7125
7564
  for (const [path, value] of Object.entries(raw)) {
7126
7565
  if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`invalid ref for ${path}`);
7127
7566
  const source = normalizeSecretSource(value.source);
7128
- const id = normalizeOptionalString$4(value.id);
7129
- const provider = normalizeOptionalString$4(value.provider);
7567
+ const id = normalizeOptionalString$6(value.id);
7568
+ const provider = normalizeOptionalString$6(value.provider);
7130
7569
  if (!source || !id) throw new Error(`invalid ref for ${path}: source/id is required`);
7131
7570
  output[path] = {
7132
7571
  source,
@@ -7213,7 +7652,7 @@ var SecretsCommands = class {
7213
7652
  if (Boolean(opts.strict) && summary.failed > 0) process.exitCode = 1;
7214
7653
  }
7215
7654
  async secretsConfigure(opts) {
7216
- const alias = normalizeOptionalString$4(opts.provider);
7655
+ const alias = normalizeOptionalString$6(opts.provider);
7217
7656
  if (!alias) throw new Error("provider alias is required");
7218
7657
  const prevConfig = loadConfig();
7219
7658
  const nextConfig = structuredClone(prevConfig);
@@ -7226,10 +7665,10 @@ var SecretsCommands = class {
7226
7665
  if (!source) throw new Error("source is required and must be one of env/file/exec");
7227
7666
  if (source === "env") nextConfig.secrets.providers[alias] = {
7228
7667
  source,
7229
- ...normalizeOptionalString$4(opts.prefix) ? { prefix: normalizeOptionalString$4(opts.prefix) } : {}
7668
+ ...normalizeOptionalString$6(opts.prefix) ? { prefix: normalizeOptionalString$6(opts.prefix) } : {}
7230
7669
  };
7231
7670
  else if (source === "file") {
7232
- const path = normalizeOptionalString$4(opts.path);
7671
+ const path = normalizeOptionalString$6(opts.path);
7233
7672
  if (!path) throw new Error("file source requires --path");
7234
7673
  nextConfig.secrets.providers[alias] = {
7235
7674
  source,
@@ -7237,13 +7676,13 @@ var SecretsCommands = class {
7237
7676
  format: "json"
7238
7677
  };
7239
7678
  } else {
7240
- const command = normalizeOptionalString$4(opts.command);
7679
+ const command = normalizeOptionalString$6(opts.command);
7241
7680
  if (!command) throw new Error("exec source requires --command");
7242
7681
  nextConfig.secrets.providers[alias] = {
7243
7682
  source,
7244
7683
  command,
7245
7684
  args: Array.isArray(opts.arg) ? opts.arg : [],
7246
- ...normalizeOptionalString$4(opts.cwd) ? { cwd: normalizeOptionalString$4(opts.cwd) } : {},
7685
+ ...normalizeOptionalString$6(opts.cwd) ? { cwd: normalizeOptionalString$6(opts.cwd) } : {},
7247
7686
  timeoutMs: parseTimeoutMs(opts.timeoutMs) ?? 5e3
7248
7687
  };
7249
7688
  }
@@ -7288,9 +7727,9 @@ var SecretsCommands = class {
7288
7727
  if (opts.remove) delete nextConfig.secrets.refs[path];
7289
7728
  else {
7290
7729
  const source = normalizeSecretSource(opts.source);
7291
- const id = normalizeOptionalString$4(opts.id);
7730
+ const id = normalizeOptionalString$6(opts.id);
7292
7731
  if (!source || !id) throw new Error("apply single ref requires --source and --id");
7293
- const provider = normalizeOptionalString$4(opts.provider);
7732
+ const provider = normalizeOptionalString$6(opts.provider);
7294
7733
  nextConfig.secrets.refs[path] = {
7295
7734
  source,
7296
7735
  id,
@@ -7636,14 +8075,94 @@ function printCronJobs(jobs) {
7636
8075
  for (const job of jobs) console.log(`${job.id} [${job.enabled ? "enabled" : "disabled"}] ${job.name} ${formatCronSchedule(job.schedule)}`);
7637
8076
  }
7638
8077
  //#endregion
8078
+ //#region src/cli/runtime-state/local-ui-runtime.store.ts
8079
+ var LocalUiRuntimeStore = class {
8080
+ get path() {
8081
+ return resolve(getDataDir(), "run", "ui-runtime.json");
8082
+ }
8083
+ read = () => {
8084
+ if (!existsSync(this.path)) return null;
8085
+ try {
8086
+ const raw = readFileSync(this.path, "utf-8");
8087
+ return JSON.parse(raw);
8088
+ } catch {
8089
+ return null;
8090
+ }
8091
+ };
8092
+ write = (state) => {
8093
+ mkdirSync(resolve(this.path, ".."), { recursive: true });
8094
+ writeFileSync(this.path, JSON.stringify(state, null, 2));
8095
+ };
8096
+ update = (updater) => {
8097
+ const current = this.read();
8098
+ if (!current) return null;
8099
+ const next = updater(current);
8100
+ this.write(next);
8101
+ return next;
8102
+ };
8103
+ clear = () => {
8104
+ if (existsSync(this.path)) rmSync(this.path, { force: true });
8105
+ };
8106
+ clearIfOwnedByProcess = (pid = process.pid) => {
8107
+ if (this.read()?.pid === pid) this.clear();
8108
+ };
8109
+ writeCurrentProcess = (uiConfig, pid = process.pid) => {
8110
+ const existing = this.read();
8111
+ const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
8112
+ const state = {
8113
+ pid,
8114
+ startedAt: existing?.pid === pid && typeof existing.startedAt === "string" ? existing.startedAt : (/* @__PURE__ */ new Date()).toISOString(),
8115
+ uiUrl,
8116
+ apiUrl: `${uiUrl}/api`,
8117
+ uiHost: uiConfig.host,
8118
+ uiPort: uiConfig.port,
8119
+ ...existing?.remote ? { remote: existing.remote } : {}
8120
+ };
8121
+ this.write(state);
8122
+ return state;
8123
+ };
8124
+ };
8125
+ const localUiRuntimeStore = new LocalUiRuntimeStore();
8126
+ //#endregion
8127
+ //#region src/cli/runtime-state/local-ui-discovery.service.ts
8128
+ var LocalUiDiscoveryService = class {
8129
+ constructor(localUiStore = localUiRuntimeStore, managedServiceStore = managedServiceStateStore, isProcessRunningFn = (pid) => isProcessRunning(pid)) {
8130
+ this.localUiStore = localUiStore;
8131
+ this.managedServiceStore = managedServiceStore;
8132
+ this.isProcessRunningFn = isProcessRunningFn;
8133
+ }
8134
+ readRunningState = (state) => {
8135
+ if (!state || !this.isProcessRunningFn(state.pid)) return null;
8136
+ return state;
8137
+ };
8138
+ readRunningRuntimeState = () => {
8139
+ return this.readRunningState(this.localUiStore.read()) ?? this.readRunningState(this.managedServiceStore.read());
8140
+ };
8141
+ resolveApiBase = () => {
8142
+ const state = this.readRunningRuntimeState();
8143
+ if (!state) return null;
8144
+ if (typeof state.uiUrl === "string" && state.uiUrl.trim().length > 0) return state.uiUrl.replace(/\/+$/, "");
8145
+ if (typeof state.apiUrl === "string" && state.apiUrl.trim().length > 0) return state.apiUrl.replace(/\/api\/?$/, "").replace(/\/+$/, "");
8146
+ return null;
8147
+ };
8148
+ resolveLocalOrigin = (config) => {
8149
+ const state = this.readRunningRuntimeState();
8150
+ const runtimePort = state && typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : null;
8151
+ if (runtimePort !== null) return resolveLocalUiBaseUrl({
8152
+ host: "0.0.0.0",
8153
+ port: runtimePort
8154
+ });
8155
+ return resolveLocalUiBaseUrl({
8156
+ host: "0.0.0.0",
8157
+ port: typeof config.ui.port === "number" && Number.isFinite(config.ui.port) ? config.ui.port : 55667
8158
+ });
8159
+ };
8160
+ };
8161
+ const localUiDiscoveryService = new LocalUiDiscoveryService();
8162
+ //#endregion
7639
8163
  //#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;
8164
+ function resolveLocalUiApiBase() {
8165
+ return localUiDiscoveryService.resolveApiBase();
7647
8166
  }
7648
8167
  var UiBridgeApiClient = class {
7649
8168
  cookie;
@@ -7690,7 +8209,7 @@ var CronCommands = class {
7690
8209
  this.local = local;
7691
8210
  }
7692
8211
  createApiClient = () => {
7693
- const apiBase = resolveManagedApiBase();
8212
+ const apiBase = resolveLocalUiApiBase();
7694
8213
  if (!apiBase) return null;
7695
8214
  return new UiBridgeApiClient(apiBase);
7696
8215
  };
@@ -7857,13 +8376,13 @@ function createUnusedRuntime(_params) {
7857
8376
  throw new Error("runtime creation is not available during runtime listing");
7858
8377
  }
7859
8378
  function loadRuntimeOnlyPluginRegistry(config, workspaceDir) {
7860
- const workspaceExtensionsDir = resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR);
8379
+ const { configWithDevPluginOverrides, excludedRoots } = resolveDevPluginLoadingContext(config, resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR));
7861
8380
  return loadOpenClawPlugins({
7862
- config: applyDevFirstPartyPluginLoadPaths(config, workspaceExtensionsDir),
8381
+ config: configWithDevPluginOverrides,
7863
8382
  workspaceDir,
7864
8383
  includeBundled: false,
7865
8384
  kinds: ["agent-runtime"],
7866
- excludeRoots: resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir),
8385
+ excludeRoots: excludedRoots,
7867
8386
  ...buildReservedPluginLoadOptions(),
7868
8387
  logger: {
7869
8388
  info: (message) => console.log(message),
@@ -8025,7 +8544,7 @@ var AgentCommands = class {
8025
8544
  //#region src/cli/commands/remote-support/remote-runtime-support.ts
8026
8545
  let currentProcessRemoteRuntimeState = null;
8027
8546
  function hasRunningNextclawManagedService() {
8028
- const state = readServiceState();
8547
+ const state = managedServiceStateStore.read();
8029
8548
  return Boolean(state && isProcessRunning(state.pid));
8030
8549
  }
8031
8550
  function createNextclawRemotePlatformClient() {
@@ -8038,7 +8557,7 @@ function createNextclawRemotePlatformClient() {
8038
8557
  requireConfigured: true
8039
8558
  }).platformBase,
8040
8559
  readManagedServiceState: () => {
8041
- const state = readServiceState();
8560
+ const state = managedServiceStateStore.read();
8042
8561
  if (!state) return null;
8043
8562
  return {
8044
8563
  pid: state.pid,
@@ -8057,9 +8576,13 @@ function createNextclawRemoteConnector(params = {}) {
8057
8576
  function createNextclawRemoteStatusStore(mode) {
8058
8577
  return new RemoteStatusStore(mode, { writeRemoteState: (next) => {
8059
8578
  currentProcessRemoteRuntimeState = next;
8060
- const serviceState = readServiceState();
8579
+ if (localUiRuntimeStore.read()?.pid === process.pid) localUiRuntimeStore.update((state) => ({
8580
+ ...state,
8581
+ remote: next
8582
+ }));
8583
+ const serviceState = managedServiceStateStore.read();
8061
8584
  if (!serviceState || serviceState.pid !== process.pid) return;
8062
- updateServiceState((state) => ({
8585
+ managedServiceStateStore.update((state) => ({
8063
8586
  ...state,
8064
8587
  remote: next
8065
8588
  }));
@@ -8069,10 +8592,12 @@ function buildNextclawConfiguredRemoteState(config) {
8069
8592
  return buildConfiguredRemoteState(config);
8070
8593
  }
8071
8594
  function readCurrentNextclawRemoteRuntimeState() {
8072
- const serviceState = readServiceState();
8073
- const currentRemoteState = currentProcessRemoteRuntimeState ?? serviceState?.remote ?? null;
8595
+ const uiRuntimeState = localUiRuntimeStore.read();
8596
+ const serviceState = managedServiceStateStore.read();
8597
+ const currentRemoteState = currentProcessRemoteRuntimeState ?? uiRuntimeState?.remote ?? serviceState?.remote ?? null;
8074
8598
  if (!currentRemoteState) return null;
8075
- if (!serviceState || isProcessRunning(serviceState.pid)) return currentRemoteState;
8599
+ const owningRuntime = uiRuntimeState ?? serviceState;
8600
+ if (!owningRuntime || isProcessRunning(owningRuntime.pid)) return currentRemoteState;
8076
8601
  return {
8077
8602
  ...currentRemoteState,
8078
8603
  state: currentRemoteState.enabled ? "disconnected" : "disabled",
@@ -8088,15 +8613,13 @@ function resolveNextclawRemoteStatusSnapshot(config) {
8088
8613
  }
8089
8614
  //#endregion
8090
8615
  //#region src/cli/commands/remote.ts
8091
- function normalizeOptionalString$3(value) {
8616
+ function normalizeOptionalString$5(value) {
8092
8617
  if (typeof value !== "string") return;
8093
8618
  const trimmed = value.trim();
8094
8619
  return trimmed.length > 0 ? trimmed : void 0;
8095
8620
  }
8096
8621
  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}`;
8622
+ return localUiDiscoveryService.resolveLocalOrigin(config);
8100
8623
  }
8101
8624
  async function probeLocalUi(localOrigin) {
8102
8625
  try {
@@ -8187,8 +8710,8 @@ var RemoteCommands = class {
8187
8710
  configuredEnabled: snapshot.configuredEnabled,
8188
8711
  runtime: snapshot.runtime,
8189
8712
  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
8713
+ deviceName: snapshot.runtime?.deviceName ?? normalizeOptionalString$5(config.remote.deviceName) ?? hostname(),
8714
+ platformBase: snapshot.runtime?.platformBase ?? normalizeOptionalString$5(config.remote.platformApiBase) ?? normalizeOptionalString$5(config.providers.nextclaw?.apiBase) ?? null
8192
8715
  };
8193
8716
  }
8194
8717
  async status(opts = {}) {
@@ -8214,8 +8737,8 @@ var RemoteCommands = class {
8214
8737
  const snapshot = resolveNextclawRemoteStatusSnapshot(config);
8215
8738
  const localOrigin = snapshot.runtime?.localOrigin ?? this.deps.currentLocalOrigin ?? resolveConfiguredLocalOrigin(config);
8216
8739
  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);
8740
+ const token = normalizeOptionalString$5(config.providers.nextclaw?.apiKey);
8741
+ const platformApiBase = normalizeOptionalString$5(config.remote.platformApiBase) ?? normalizeOptionalString$5(config.providers.nextclaw?.apiBase);
8219
8742
  const checks = [
8220
8743
  {
8221
8744
  name: "remote-enabled",
@@ -8324,7 +8847,7 @@ var DiagnosticsCommands = class {
8324
8847
  constructor(deps) {
8325
8848
  this.deps = deps;
8326
8849
  }
8327
- async status(opts = {}) {
8850
+ status = async (opts = {}) => {
8328
8851
  const report = await this.collectRuntimeStatus({
8329
8852
  verbose: Boolean(opts.verbose),
8330
8853
  fix: Boolean(opts.fix)
@@ -8340,30 +8863,58 @@ var DiagnosticsCommands = class {
8340
8863
  verbose: Boolean(opts.verbose)
8341
8864
  });
8342
8865
  process.exitCode = 0;
8343
- }
8344
- async doctor(opts = {}) {
8866
+ };
8867
+ doctor = async (opts = {}) => {
8345
8868
  const report = await this.collectRuntimeStatus({
8346
8869
  verbose: Boolean(opts.verbose),
8347
8870
  fix: Boolean(opts.fix)
8348
8871
  });
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
- })()
8359
- });
8360
- const providerConfigured = report.providers.some((provider) => provider.configured);
8361
- const checks = [
8362
- {
8363
- name: "config-file",
8364
- status: report.configExists ? "pass" : "fail",
8365
- detail: report.configPath
8366
- },
8872
+ const checkPort = await this.checkPortAvailability(this.resolveDoctorPortCheckTarget(report));
8873
+ const checks = this.buildDoctorChecks(report, checkPort);
8874
+ const exitCode = this.resolveDoctorExitCode(checks);
8875
+ if (opts.json) {
8876
+ console.log(JSON.stringify({
8877
+ generatedAt: report.generatedAt,
8878
+ checks,
8879
+ status: report,
8880
+ exitCode
8881
+ }, null, 2));
8882
+ process.exitCode = exitCode;
8883
+ return;
8884
+ }
8885
+ printDoctorReport({
8886
+ logo: this.deps.logo,
8887
+ generatedAt: report.generatedAt,
8888
+ checks,
8889
+ recommendations: report.recommendations,
8890
+ verbose: Boolean(opts.verbose),
8891
+ logTail: report.logTail
8892
+ });
8893
+ process.exitCode = exitCode;
8894
+ };
8895
+ resolveDoctorPortCheckTarget = (report) => {
8896
+ const host = report.process.running && report.endpoints.uiUrl ? new URL(report.endpoints.uiUrl).hostname : "127.0.0.1";
8897
+ try {
8898
+ const base = report.process.running && report.endpoints.uiUrl ? report.endpoints.uiUrl : report.endpoints.configuredUiUrl;
8899
+ return {
8900
+ host,
8901
+ port: Number(new URL(base).port || 80)
8902
+ };
8903
+ } catch {
8904
+ return {
8905
+ host,
8906
+ port: 55667
8907
+ };
8908
+ }
8909
+ };
8910
+ buildDoctorChecks = (report, checkPort) => {
8911
+ const providerConfigured = report.providers.some((provider) => provider.configured);
8912
+ return [
8913
+ {
8914
+ name: "config-file",
8915
+ status: report.configExists ? "pass" : "fail",
8916
+ detail: report.configPath
8917
+ },
8367
8918
  {
8368
8919
  name: "workspace-dir",
8369
8920
  status: report.workspaceExists ? "pass" : "warn",
@@ -8376,12 +8927,12 @@ var DiagnosticsCommands = class {
8376
8927
  },
8377
8928
  {
8378
8929
  name: "service-health",
8379
- status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : report.health.configured.state === "ok" ? "warn" : "warn",
8930
+ status: report.process.running ? report.health.managed.state === "ok" ? "pass" : "fail" : "warn",
8380
8931
  detail: report.process.running ? `${report.health.managed.state}: ${report.health.managed.detail}` : `${report.health.configured.state}: ${report.health.configured.detail}`
8381
8932
  },
8382
8933
  {
8383
8934
  name: "ui-port-availability",
8384
- status: report.process.running ? "pass" : checkPort.available ? "pass" : "fail",
8935
+ status: report.process.running || checkPort.available ? "pass" : "fail",
8385
8936
  detail: report.process.running ? "managed by running service" : checkPort.available ? "available" : checkPort.detail
8386
8937
  },
8387
8938
  {
@@ -8390,40 +8941,23 @@ var DiagnosticsCommands = class {
8390
8941
  detail: providerConfigured ? "at least one provider configured" : "no provider api key configured"
8391
8942
  }
8392
8943
  ];
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) {
8944
+ };
8945
+ resolveDoctorExitCode = (checks) => {
8946
+ if (checks.some((check) => check.status === "fail")) return 1;
8947
+ if (checks.some((check) => check.status === "warn")) return 1;
8948
+ return 0;
8949
+ };
8950
+ collectRuntimeStatus = async (params) => {
8417
8951
  const configPath = getConfigPath();
8418
8952
  const config = loadConfig();
8419
8953
  const workspacePath = getWorkspacePath(config.agents.defaults.workspace);
8420
- const serviceStatePath = resolve(getDataDir(), "run", "service.json");
8954
+ const serviceStatePath = managedServiceStateStore.path;
8421
8955
  const fixActions = [];
8422
- let serviceState = readServiceState();
8956
+ let serviceState = managedServiceStateStore.read();
8423
8957
  if (params.fix && serviceState && !isProcessRunning(serviceState.pid)) {
8424
- clearServiceState();
8958
+ managedServiceStateStore.clear();
8425
8959
  fixActions.push("Cleared stale service state file.");
8426
- serviceState = readServiceState();
8960
+ serviceState = managedServiceStateStore.read();
8427
8961
  }
8428
8962
  const managedByState = Boolean(serviceState);
8429
8963
  const running = Boolean(serviceState && isProcessRunning(serviceState.pid));
@@ -8459,7 +8993,7 @@ var DiagnosticsCommands = class {
8459
8993
  issues,
8460
8994
  recommendations
8461
8995
  });
8462
- const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveServiceLogPath(), 25) : [];
8996
+ const logTail = params.verbose ? this.readLogTail(serviceState?.logPath ?? resolveAppLogPath("service"), 25) : [];
8463
8997
  const level = running ? managedHealth.state === "ok" ? issues.length > 0 ? "degraded" : "healthy" : "degraded" : "stopped";
8464
8998
  return {
8465
8999
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -8497,8 +9031,8 @@ var DiagnosticsCommands = class {
8497
9031
  level,
8498
9032
  exitCode: 0
8499
9033
  };
8500
- }
8501
- async probeApiHealth(url, timeoutMs = 1500) {
9034
+ };
9035
+ probeApiHealth = async (url, timeoutMs = 1500) => {
8502
9036
  const controller = new AbortController();
8503
9037
  const timer = setTimeout(() => controller.abort(), timeoutMs);
8504
9038
  try {
@@ -8529,8 +9063,8 @@ var DiagnosticsCommands = class {
8529
9063
  } finally {
8530
9064
  clearTimeout(timer);
8531
9065
  }
8532
- }
8533
- listProviderStatuses(config) {
9066
+ };
9067
+ listProviderStatuses = (config) => {
8534
9068
  return listBuiltinProviders().map((spec) => {
8535
9069
  const provider = config.providers[spec.name];
8536
9070
  const apiKeyRefSet = hasSecretRef(config, `providers.${spec.name}.apiKey`);
@@ -8555,8 +9089,8 @@ var DiagnosticsCommands = class {
8555
9089
  detail: provider.apiKey ? "apiKey set" : apiKeyRefSet ? "apiKey ref set" : "apiKey not set"
8556
9090
  };
8557
9091
  });
8558
- }
8559
- collectRuntimeIssues(params) {
9092
+ };
9093
+ collectRuntimeIssues = (params) => {
8560
9094
  if (!existsSync(params.configPath)) {
8561
9095
  params.issues.push("Config file is missing.");
8562
9096
  params.recommendations.push(`Run ${APP_NAME} init to create config files.`);
@@ -8571,7 +9105,7 @@ var DiagnosticsCommands = class {
8571
9105
  }
8572
9106
  if (params.running && params.managedHealth.state !== "ok") {
8573
9107
  params.issues.push(`Managed service health check failed: ${params.managedHealth.detail}`);
8574
- params.recommendations.push(`Check logs at ${params.serviceState?.logPath ?? resolveServiceLogPath()}.`);
9108
+ params.recommendations.push(`Check logs at ${params.serviceState?.logPath ?? resolveAppLogPath("service")}.`);
8575
9109
  }
8576
9110
  if (params.running && params.serviceState?.startupState === "degraded" && params.managedHealth.state !== "ok") {
8577
9111
  const startupHint = params.serviceState.startupLastProbeError ? ` (${params.serviceState.startupLastProbeError})` : "";
@@ -8584,8 +9118,8 @@ var DiagnosticsCommands = class {
8584
9118
  params.recommendations.push("Another process may be occupying the UI port; stop it or use --ui-port with a free port.");
8585
9119
  }
8586
9120
  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) {
9121
+ };
9122
+ readLogTail = (path, maxLines = 25) => {
8589
9123
  if (!existsSync(path)) return [];
8590
9124
  try {
8591
9125
  const lines = readFileSync(path, "utf-8").split(/\r?\n/).filter(Boolean);
@@ -8594,8 +9128,8 @@ var DiagnosticsCommands = class {
8594
9128
  } catch {
8595
9129
  return [];
8596
9130
  }
8597
- }
8598
- async checkPortAvailability(params) {
9131
+ };
9132
+ checkPortAvailability = async (params) => {
8599
9133
  return await new Promise((resolve) => {
8600
9134
  const server = createServer();
8601
9135
  server.once("error", (error) => {
@@ -8613,7 +9147,34 @@ var DiagnosticsCommands = class {
8613
9147
  });
8614
9148
  });
8615
9149
  });
8616
- }
9150
+ };
9151
+ };
9152
+ //#endregion
9153
+ //#region src/cli/commands/logs.ts
9154
+ var LogsCommands = class {
9155
+ constructor(runtime = getLoggingRuntime()) {
9156
+ this.runtime = runtime;
9157
+ }
9158
+ logsPath = () => {
9159
+ const paths = this.runtime.getPaths();
9160
+ console.log([
9161
+ `Logs directory: ${paths.logsDir}`,
9162
+ `Service log: ${paths.serviceLogPath}`,
9163
+ `Crash log: ${paths.crashLogPath}`,
9164
+ `Archive: ${paths.archiveDir}`
9165
+ ].join("\n"));
9166
+ };
9167
+ logsTail = (opts = {}) => {
9168
+ const kind = opts.crash ? "crash" : "service";
9169
+ const rawLines = Number(opts.lines);
9170
+ const lines = Number.isFinite(rawLines) && rawLines > 0 ? Math.floor(rawLines) : 40;
9171
+ const output = this.runtime.tail(kind, lines);
9172
+ if (output.length === 0) {
9173
+ console.log(`No log entries found in ${this.runtime.resolveLogPath(kind)}.`);
9174
+ return;
9175
+ }
9176
+ console.log(output.join("\n"));
9177
+ };
8617
9178
  };
8618
9179
  //#endregion
8619
9180
  //#region src/cli/commands/service-support/runtime/service-port-probe.ts
@@ -8732,17 +9293,42 @@ async function probeHealthEndpoint(healthUrl) {
8732
9293
  req.end();
8733
9294
  });
8734
9295
  }
9296
+ async function inspectUiTarget(params) {
9297
+ const { checkPortAvailabilityFn, healthUrl, host, port, probeHealthEndpointFn } = params;
9298
+ const availability = await (checkPortAvailabilityFn ?? checkPortAvailability)({
9299
+ host,
9300
+ port
9301
+ });
9302
+ if (availability.available) return {
9303
+ state: "available",
9304
+ availabilityDetail: availability.detail,
9305
+ probeError: null
9306
+ };
9307
+ const probe = await (probeHealthEndpointFn ?? probeHealthEndpoint)(healthUrl);
9308
+ if (probe.healthy) return {
9309
+ state: "healthy-existing",
9310
+ availabilityDetail: availability.detail,
9311
+ probeError: null
9312
+ };
9313
+ return {
9314
+ state: "occupied-unhealthy",
9315
+ availabilityDetail: availability.detail,
9316
+ probeError: probe.error
9317
+ };
9318
+ }
8735
9319
  async function describeUnmanagedHealthyTargetMessage(params) {
8736
9320
  const uiConfig = resolveUiConfig(loadConfig(), params.uiOverrides);
8737
9321
  const healthUrl = `${resolveUiApiBase(uiConfig.host, uiConfig.port)}/api/health`;
8738
- if ((await (params.checkPortAvailabilityFn ?? checkPortAvailability)({
9322
+ if ((await inspectUiTarget({
8739
9323
  host: uiConfig.host,
8740
- port: uiConfig.port
8741
- })).available) return null;
8742
- if (!(await (params.probeHealthEndpointFn ?? probeHealthEndpoint)(healthUrl)).healthy) return null;
9324
+ port: uiConfig.port,
9325
+ healthUrl,
9326
+ checkPortAvailabilityFn: params.checkPortAvailabilityFn,
9327
+ probeHealthEndpointFn: params.probeHealthEndpointFn
9328
+ })).state !== "healthy-existing") return null;
8743
9329
  return [
8744
9330
  `Target UI health: ${healthUrl}`,
8745
- `A healthy ${APP_NAME} service is already responding on this port, but it is not tracked by ${resolveServiceStatePath()}.`,
9331
+ `A healthy ${APP_NAME} service is already responding on this port, but it is not tracked by ${managedServiceStateStore.path}.`,
8746
9332
  `${APP_NAME} restart only stops the background service recorded in managed state; it will not auto-kill Docker or other external listeners.`,
8747
9333
  `Fix: stop that external service first or rerun with --ui-port <port>.`
8748
9334
  ].join("\n");
@@ -9076,7 +9662,7 @@ function claimManagedRemoteRuntimeOwnership(params) {
9076
9662
  const managedConflict = detectManagedRemoteOwnershipConflict({
9077
9663
  currentPid,
9078
9664
  isProcessRunningFn,
9079
- readServiceStateFn: params.readServiceStateFn ?? readServiceState
9665
+ readServiceStateFn: params.readServiceStateFn ?? managedServiceStateStore.read
9080
9666
  });
9081
9667
  if (managedConflict) return {
9082
9668
  ok: false,
@@ -9119,7 +9705,7 @@ function createManagedRemoteModuleForUi(params) {
9119
9705
  });
9120
9706
  }
9121
9707
  function writeInitialManagedServiceState(params) {
9122
- writeServiceState({
9708
+ managedServiceStateStore.write({
9123
9709
  pid: params.snapshot.pid,
9124
9710
  startedAt: (/* @__PURE__ */ new Date()).toISOString(),
9125
9711
  uiUrl: params.snapshot.uiUrl,
@@ -9134,7 +9720,7 @@ function writeInitialManagedServiceState(params) {
9134
9720
  });
9135
9721
  }
9136
9722
  function writeReadyManagedServiceState(params) {
9137
- const currentState = readServiceState();
9723
+ const currentState = managedServiceStateStore.read();
9138
9724
  const state = {
9139
9725
  pid: params.snapshot.pid,
9140
9726
  startedAt: currentState?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
@@ -9149,7 +9735,7 @@ function writeReadyManagedServiceState(params) {
9149
9735
  startupCheckedAt: (/* @__PURE__ */ new Date()).toISOString(),
9150
9736
  ...currentState?.remote ? { remote: currentState.remote } : {}
9151
9737
  };
9152
- writeServiceState(state);
9738
+ managedServiceStateStore.write(state);
9153
9739
  return state;
9154
9740
  }
9155
9741
  //#endregion
@@ -9197,8 +9783,8 @@ function resolveSessionRouteCandidate(params) {
9197
9783
  function spawnManagedService(params) {
9198
9784
  const { appName, config, uiConfig, uiUrl, apiUrl, healthUrl, startupTimeoutMs, resolveStartupTimeoutMs, appendStartupStage, printStartupFailureDiagnostics, resolveServiceLogPath } = params;
9199
9785
  const logPath = resolveServiceLogPath();
9200
- mkdirSync(resolve(logPath, ".."), { recursive: true });
9201
- const logFd = openSync(logPath, "a");
9786
+ new FileLogSink({ serviceLogPath: logPath }).ensureReady();
9787
+ mkdirSync(dirname(logPath), { recursive: true });
9202
9788
  const readinessTimeoutMs = resolveStartupTimeoutMs(startupTimeoutMs);
9203
9789
  const quickPhaseTimeoutMs = Math.min(8e3, readinessTimeoutMs);
9204
9790
  const extendedPhaseTimeoutMs = Math.max(0, readinessTimeoutMs - quickPhaseTimeoutMs);
@@ -9218,15 +9804,10 @@ function spawnManagedService(params) {
9218
9804
  appendStartupStage(logPath, `spawning background process: ${cliLaunch.command} ${childArgs.join(" ")}`);
9219
9805
  const child = spawn(cliLaunch.command, childArgs, {
9220
9806
  env: process.env,
9221
- stdio: [
9222
- "ignore",
9223
- logFd,
9224
- logFd
9225
- ],
9807
+ stdio: "ignore",
9226
9808
  detached: true
9227
9809
  });
9228
9810
  appendStartupStage(logPath, `spawned background process pid=${child.pid ?? "unknown"}`);
9229
- closeSync(logFd);
9230
9811
  if (!child.pid) {
9231
9812
  appendStartupStage(logPath, "spawn failed: child pid missing");
9232
9813
  console.error("Error: Failed to start background service.");
@@ -9371,32 +9952,10 @@ var ServiceFileWatcherRegistry = class {
9371
9952
  }));
9372
9953
  };
9373
9954
  };
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
9955
  function finalizeLocalUiStartup(params) {
9397
9956
  const { setUiEventPublisher, uiConfig, uiStartup } = params;
9398
9957
  setUiEventPublisher(uiStartup?.publish);
9399
- if (uiStartup) writeLocalServiceDiscoveryState(uiConfig);
9958
+ if (uiStartup) localUiRuntimeStore.writeCurrentProcess(uiConfig);
9400
9959
  }
9401
9960
  async function startGatewayRuntimeSupport(params) {
9402
9961
  const { cronJobs, cronStorePath, reloadCronStore, remoteModule, startCron, startHeartbeat, watchConfigFile, watcherRegistry } = params;
@@ -9423,7 +9982,7 @@ function resolveRemoteServiceView(currentUi) {
9423
9982
  uiUrl: resolveUiApiBase(currentUi.host, currentUi.port),
9424
9983
  uiPort: currentUi.port
9425
9984
  };
9426
- const serviceState = readServiceState();
9985
+ const serviceState = managedServiceStateStore.read();
9427
9986
  const serviceRunning = Boolean(serviceState && isProcessRunning(serviceState.pid));
9428
9987
  return {
9429
9988
  running: serviceRunning,
@@ -9462,7 +10021,7 @@ async function controlCurrentProcessRuntime(action, controller) {
9462
10021
  };
9463
10022
  }
9464
10023
  async function controlManagedService(action, deps) {
9465
- const state = readServiceState();
10024
+ const state = managedServiceStateStore.read();
9466
10025
  const running = Boolean(state && isProcessRunning(state.pid));
9467
10026
  const currentProcess = Boolean(running && state?.pid === process.pid);
9468
10027
  const uiOverrides = resolveManagedUiOverrides();
@@ -9548,7 +10107,7 @@ function launchManagedSelfControl(params = {}) {
9548
10107
  "const { spawn } = require(\"node:child_process\");",
9549
10108
  "const { rmSync } = require(\"node:fs\");",
9550
10109
  `const parentPid = ${process.pid};`,
9551
- `const serviceStatePath = ${JSON.stringify(resolveServiceStatePath())};`,
10110
+ `const serviceStatePath = ${JSON.stringify(managedServiceStateStore.path)};`,
9552
10111
  `const command = ${JSON.stringify(params.command ?? null)};`,
9553
10112
  `const args = ${JSON.stringify(params.args ?? [])};`,
9554
10113
  `const cwd = ${JSON.stringify(process.cwd())};`,
@@ -9597,7 +10156,7 @@ function launchManagedSelfControl(params = {}) {
9597
10156
  }
9598
10157
  //#endregion
9599
10158
  //#region src/cli/commands/remote-support/remote-access-host.ts
9600
- function normalizeOptionalString$2(value) {
10159
+ function normalizeOptionalString$4(value) {
9601
10160
  if (typeof value !== "string") return null;
9602
10161
  const trimmed = value.trim();
9603
10162
  return trimmed.length > 0 ? trimmed : null;
@@ -9626,8 +10185,8 @@ var RemoteAccessHost = class {
9626
10185
  const status = this.deps.remoteCommands.getStatusView();
9627
10186
  return {
9628
10187
  account: this.readAccountView({
9629
- token: normalizeOptionalString$2(config.providers.nextclaw?.apiKey),
9630
- apiBase: normalizeOptionalString$2(config.providers.nextclaw?.apiBase),
10188
+ token: normalizeOptionalString$4(config.providers.nextclaw?.apiKey),
10189
+ apiBase: normalizeOptionalString$4(config.providers.nextclaw?.apiBase),
9631
10190
  platformBase: status.platformBase
9632
10191
  }),
9633
10192
  settings: {
@@ -9658,7 +10217,7 @@ var RemoteAccessHost = class {
9658
10217
  pollBrowserAuth = async (input) => {
9659
10218
  const config = loadConfig(getConfigPath());
9660
10219
  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,
10220
+ apiBase: normalizeOptionalString$4(input.apiBase) ?? normalizeOptionalString$4(config.remote.platformApiBase) ?? normalizeOptionalString$4(config.providers.nextclaw?.apiBase) ?? void 0,
9662
10221
  sessionId: input.sessionId
9663
10222
  });
9664
10223
  if (result.status !== "authorized") return result;
@@ -9784,51 +10343,71 @@ var AssetPutTool = class {
9784
10343
  description = "Put a normal file path or base64 bytes into the managed asset store.";
9785
10344
  parameters = {
9786
10345
  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."
10346
+ oneOf: [{
10347
+ type: "object",
10348
+ properties: {
10349
+ path: {
10350
+ type: "string",
10351
+ description: "Existing local file path to put into the asset store."
10352
+ },
10353
+ fileName: {
10354
+ type: "string",
10355
+ description: "Optional asset file name override."
10356
+ },
10357
+ mimeType: {
10358
+ type: "string",
10359
+ description: "Optional mime type override."
10360
+ }
9795
10361
  },
9796
- fileName: {
9797
- type: "string",
9798
- description: "Optional asset file name override. Required when using bytesBase64."
10362
+ required: ["path"],
10363
+ additionalProperties: false
10364
+ }, {
10365
+ type: "object",
10366
+ properties: {
10367
+ bytesBase64: {
10368
+ type: "string",
10369
+ description: "Base64 file bytes. Use together with fileName when no source path exists."
10370
+ },
10371
+ fileName: {
10372
+ type: "string",
10373
+ description: "Asset file name. Required when using bytesBase64."
10374
+ },
10375
+ mimeType: {
10376
+ type: "string",
10377
+ description: "Optional mime type override."
10378
+ }
9799
10379
  },
9800
- mimeType: {
9801
- type: "string",
9802
- description: "Optional mime type override."
9803
- }
9804
- }
10380
+ required: ["bytesBase64", "fileName"],
10381
+ additionalProperties: false
10382
+ }]
9805
10383
  };
9806
10384
  constructor(assetStore, contentBasePath) {
9807
10385
  this.assetStore = assetStore;
9808
10386
  this.contentBasePath = contentBasePath;
9809
10387
  }
9810
- async execute(args) {
10388
+ execute = async (args) => {
9811
10389
  const path = readOptionalString$5(args?.path);
9812
10390
  const fileName = readOptionalString$5(args?.fileName);
9813
10391
  const mimeType = readOptionalString$5(args?.mimeType);
9814
10392
  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 {
10393
+ if (path) return {
9828
10394
  ok: true,
9829
- asset: toAssetPayload(record, this.contentBasePath)
10395
+ asset: toAssetPayload(await this.assetStore.putPath({
10396
+ path,
10397
+ fileName: fileName ?? void 0,
10398
+ mimeType
10399
+ }), this.contentBasePath)
9830
10400
  };
9831
- }
10401
+ if (bytes && fileName) return {
10402
+ ok: true,
10403
+ asset: toAssetPayload(await this.assetStore.putBytes({
10404
+ fileName,
10405
+ mimeType,
10406
+ bytes
10407
+ }), this.contentBasePath)
10408
+ };
10409
+ throw new Error("asset_put received invalid arguments after validation.");
10410
+ };
9832
10411
  };
9833
10412
  var AssetExportTool = class {
9834
10413
  name = "asset_export";
@@ -9845,12 +10424,13 @@ var AssetExportTool = class {
9845
10424
  description: "Destination file path."
9846
10425
  }
9847
10426
  },
9848
- required: ["assetUri", "targetPath"]
10427
+ required: ["assetUri", "targetPath"],
10428
+ additionalProperties: false
9849
10429
  };
9850
10430
  constructor(assetStore) {
9851
10431
  this.assetStore = assetStore;
9852
10432
  }
9853
- async execute(args) {
10433
+ execute = async (args) => {
9854
10434
  const assetUri = readOptionalString$5(args?.assetUri);
9855
10435
  const targetPath = readOptionalString$5(args?.targetPath);
9856
10436
  if (!assetUri || !targetPath) throw new Error("asset_export requires assetUri and targetPath.");
@@ -9859,7 +10439,7 @@ var AssetExportTool = class {
9859
10439
  assetUri,
9860
10440
  exportedPath: await this.assetStore.export({ uri: assetUri }, resolve(targetPath))
9861
10441
  };
9862
- }
10442
+ };
9863
10443
  };
9864
10444
  var AssetStatTool = class {
9865
10445
  name = "asset_stat";
@@ -9870,13 +10450,14 @@ var AssetStatTool = class {
9870
10450
  type: "string",
9871
10451
  description: "Managed asset URI to inspect."
9872
10452
  } },
9873
- required: ["assetUri"]
10453
+ required: ["assetUri"],
10454
+ additionalProperties: false
9874
10455
  };
9875
10456
  constructor(assetStore, contentBasePath) {
9876
10457
  this.assetStore = assetStore;
9877
10458
  this.contentBasePath = contentBasePath;
9878
10459
  }
9879
- async execute(args) {
10460
+ execute = async (args) => {
9880
10461
  const assetUri = readOptionalString$5(args?.assetUri);
9881
10462
  if (!assetUri) throw new Error("asset_stat requires assetUri.");
9882
10463
  const record = await this.assetStore.statRecord(assetUri);
@@ -9891,7 +10472,7 @@ var AssetStatTool = class {
9891
10472
  ok: true,
9892
10473
  asset: toAssetPayload(record, this.contentBasePath)
9893
10474
  };
9894
- }
10475
+ };
9895
10476
  };
9896
10477
  function createAssetTools(params) {
9897
10478
  const contentBasePath = params.contentBasePath ?? "/api/ncp/assets/content";
@@ -10042,7 +10623,7 @@ function toLegacyMessages(messages, options = {}) {
10042
10623
  }
10043
10624
  //#endregion
10044
10625
  //#region src/cli/commands/ncp/context/nextclaw-ncp-session-preferences.ts
10045
- function normalizeOptionalString$1(value) {
10626
+ function normalizeOptionalString$3(value) {
10046
10627
  if (typeof value !== "string") return;
10047
10628
  const trimmed = value.trim();
10048
10629
  return trimmed.length > 0 ? trimmed : void 0;
@@ -10055,7 +10636,7 @@ function readMetadataModel(metadata) {
10055
10636
  metadata.session_model
10056
10637
  ];
10057
10638
  for (const candidate of candidates) {
10058
- const normalized = normalizeOptionalString$1(candidate);
10639
+ const normalized = normalizeOptionalString$3(candidate);
10059
10640
  if (normalized) return normalized;
10060
10641
  }
10061
10642
  return null;
@@ -10082,7 +10663,7 @@ function resolveEffectiveModel(params) {
10082
10663
  if (params.requestMetadata.clear_model === true || params.requestMetadata.reset_model === true) delete params.session.metadata.preferred_model;
10083
10664
  const inboundModel = readMetadataModel(params.requestMetadata);
10084
10665
  if (inboundModel) params.session.metadata.preferred_model = inboundModel;
10085
- return normalizeOptionalString$1(params.session.metadata.preferred_model) ?? params.fallbackModel;
10666
+ return normalizeOptionalString$3(params.session.metadata.preferred_model) ?? params.fallbackModel;
10086
10667
  }
10087
10668
  function syncSessionThinkingPreference(params) {
10088
10669
  if (params.requestMetadata.clear_thinking === true || params.requestMetadata.reset_thinking === true) delete params.session.metadata.preferred_thinking;
@@ -10094,8 +10675,8 @@ function syncSessionThinkingPreference(params) {
10094
10675
  if (inboundThinking) params.session.metadata.preferred_thinking = inboundThinking;
10095
10676
  }
10096
10677
  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";
10678
+ const channel = normalizeOptionalString$3(params.requestMetadata.channel) ?? normalizeOptionalString$3(params.session.metadata.last_channel) ?? "ui";
10679
+ const chatId = normalizeOptionalString$3(params.requestMetadata.chatId) ?? normalizeOptionalString$3(params.requestMetadata.chat_id) ?? normalizeOptionalString$3(params.session.metadata.last_to) ?? "web-ui";
10099
10680
  params.session.metadata.last_channel = channel;
10100
10681
  params.session.metadata.last_to = chatId;
10101
10682
  return {
@@ -10739,7 +11320,7 @@ var NextclawNcpContextBuilder = class {
10739
11320
  accountId: accountId ?? null
10740
11321
  });
10741
11322
  const toolDefinitions = this.options.toolRegistry.getToolDefinitions();
10742
- const additionalSystemSections = [buildSessionOrchestrationSection()];
11323
+ const additionalSystemSections = [buildSessionOrchestrationSection(), buildMinimalSystemExecutionPrompt(effectiveModel)].filter(Boolean);
10743
11324
  const contextBuilder = new ContextBuilder(effectiveWorkspace, config.agents.context, {
10744
11325
  hostWorkspace: profile.workspace,
10745
11326
  sessionProjectRoot: readSessionProjectRoot(session.metadata)
@@ -11302,12 +11883,6 @@ function extractSessionMessageText(message) {
11302
11883
  if (parts.length === 0) return;
11303
11884
  return parts.join("\n\n");
11304
11885
  }
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
11886
  function readParentSessionId(metadata) {
11312
11887
  return readOptionalString$1(metadata?.["parent_session_id"]) ?? void 0;
11313
11888
  }
@@ -11527,8 +12102,6 @@ var SessionRequestBroker = class {
11527
12102
  task
11528
12103
  });
11529
12104
  if (streamedMessage) return streamedMessage;
11530
- const fallbackMessage = findLatestAssistantMessage(await backend.listSessionMessages(request.targetSessionId));
11531
- if (fallbackMessage) return fallbackMessage;
11532
12105
  throw new Error("Session request completed without a final reply.");
11533
12106
  };
11534
12107
  appendRequestEvents = (request, type) => {
@@ -11710,33 +12283,151 @@ var SessionRequestDeliveryService = class {
11710
12283
  };
11711
12284
  };
11712
12285
  //#endregion
12286
+ //#region src/cli/commands/shared/llm-usage-observer.ts
12287
+ var LlmUsageObserver = class {
12288
+ constructor(recorder, source) {
12289
+ this.recorder = recorder;
12290
+ this.source = source;
12291
+ }
12292
+ observe = (params) => {
12293
+ return this.recorder.record({
12294
+ source: this.source,
12295
+ model: params.model ?? null,
12296
+ usage: params.usage
12297
+ });
12298
+ };
12299
+ };
12300
+ var ObservedProviderManager = class extends ProviderManager {
12301
+ constructor(delegate, observer) {
12302
+ super(delegate.get(null));
12303
+ this.delegate = delegate;
12304
+ this.observer = observer;
12305
+ }
12306
+ get(model) {
12307
+ return this.delegate.get(model);
12308
+ }
12309
+ set(next) {
12310
+ this.delegate.set(next);
12311
+ }
12312
+ setConfig(nextConfig) {
12313
+ this.delegate.setConfig(nextConfig);
12314
+ }
12315
+ async chat(params) {
12316
+ const response = await this.delegate.chat(params);
12317
+ this.observer.observe({
12318
+ model: params.model ?? null,
12319
+ usage: response.usage
12320
+ });
12321
+ return response;
12322
+ }
12323
+ async *chatStream(params) {
12324
+ for await (const event of this.delegate.chatStream(params)) {
12325
+ if (event.type === "done") this.observer.observe({
12326
+ model: params.model ?? null,
12327
+ usage: event.response.usage
12328
+ });
12329
+ yield event;
12330
+ }
12331
+ }
12332
+ };
12333
+ //#endregion
11713
12334
  //#region src/cli/commands/ncp/runtime/ui-ncp-agent-handle.ts
11714
12335
  function createUiNcpAgentHandle(params) {
12336
+ const { backend, runtimeRegistry, refreshPluginRuntimeRegistrations, applyExtensionRegistry, applyMcpConfig, dispose, assetStore } = params;
11715
12337
  return {
11716
12338
  basePath: "/api/ncp/agent",
11717
- agentClientEndpoint: createAgentClientFromServer(params.backend),
11718
- streamProvider: params.backend,
11719
- sessionApi: params.backend,
12339
+ agentClientEndpoint: createAgentClientFromServer(backend),
12340
+ streamProvider: backend,
12341
+ runApi: backend,
12342
+ sessionApi: backend,
11720
12343
  listSessionTypes: (describeParams) => {
11721
- params.refreshPluginRuntimeRegistrations();
11722
- return params.runtimeRegistry.listSessionTypes(describeParams);
12344
+ refreshPluginRuntimeRegistrations();
12345
+ return runtimeRegistry.listSessionTypes(describeParams);
11723
12346
  },
11724
12347
  assetApi: {
11725
- put: (input) => params.assetStore.putBytes({
12348
+ put: (input) => assetStore.putBytes({
11726
12349
  fileName: input.fileName,
11727
12350
  mimeType: input.mimeType,
11728
12351
  bytes: input.bytes,
11729
12352
  createdAt: input.createdAt
11730
12353
  }),
11731
- stat: (uri) => params.assetStore.statRecord(uri),
11732
- resolveContentPath: (uri) => params.assetStore.resolveContentPath(uri)
12354
+ stat: (uri) => assetStore.statRecord(uri),
12355
+ resolveContentPath: (uri) => assetStore.resolveContentPath(uri)
11733
12356
  },
11734
- applyExtensionRegistry: params.applyExtensionRegistry,
11735
- applyMcpConfig: params.applyMcpConfig,
11736
- dispose: params.dispose
12357
+ applyExtensionRegistry,
12358
+ applyMcpConfig,
12359
+ dispose
11737
12360
  };
11738
12361
  }
11739
12362
  //#endregion
12363
+ //#region src/cli/runtime-state/llm-usage-record.ts
12364
+ var LlmUsageRecordFactory = class {
12365
+ create = (params) => {
12366
+ const { observedAt, source, model, usage: rawUsage } = params;
12367
+ const usage = this.sanitizeUsage(rawUsage);
12368
+ return {
12369
+ version: 1,
12370
+ observedAt: observedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
12371
+ source,
12372
+ model: this.normalizeModel(model),
12373
+ usage,
12374
+ summary: this.buildSummary(usage)
12375
+ };
12376
+ };
12377
+ sanitizeUsage = (usage) => {
12378
+ const next = {};
12379
+ for (const [key, value] of Object.entries(usage)) {
12380
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) continue;
12381
+ next[key] = Math.floor(value);
12382
+ }
12383
+ return next;
12384
+ };
12385
+ buildSummary = (usage) => {
12386
+ const promptTokens = usage.prompt_tokens ?? usage.input_tokens ?? 0;
12387
+ const completionTokens = usage.completion_tokens ?? usage.output_tokens ?? 0;
12388
+ const totalTokens = usage.total_tokens ?? promptTokens + completionTokens;
12389
+ const cacheMetricKeys = Object.keys(usage).filter((key) => key.endsWith("cached_tokens"));
12390
+ const cachedTokens = cacheMetricKeys.reduce((max, key) => Math.max(max, usage[key] ?? 0), 0);
12391
+ return {
12392
+ promptTokens,
12393
+ completionTokens,
12394
+ totalTokens,
12395
+ cachedTokens,
12396
+ cacheHit: cachedTokens > 0,
12397
+ cacheMetricKeys
12398
+ };
12399
+ };
12400
+ normalizeModel = (value) => {
12401
+ if (typeof value !== "string") return null;
12402
+ const trimmed = value.trim();
12403
+ return trimmed.length > 0 ? trimmed : null;
12404
+ };
12405
+ };
12406
+ const llmUsageRecordFactory = new LlmUsageRecordFactory();
12407
+ //#endregion
12408
+ //#region src/cli/commands/shared/llm-usage-recorder.ts
12409
+ var LlmUsageRecorder = class {
12410
+ constructor(deps = {}) {
12411
+ this.deps = deps;
12412
+ }
12413
+ record = (params) => {
12414
+ const record = this.recordFactory.create(params);
12415
+ this.snapshotStore.write(record);
12416
+ this.historyStore.append(record);
12417
+ return record;
12418
+ };
12419
+ get snapshotStore() {
12420
+ return this.deps.snapshotStore ?? llmUsageSnapshotStore;
12421
+ }
12422
+ get historyStore() {
12423
+ return this.deps.historyStore ?? llmUsageHistoryStore;
12424
+ }
12425
+ get recordFactory() {
12426
+ return this.deps.recordFactory ?? llmUsageRecordFactory;
12427
+ }
12428
+ };
12429
+ const llmUsageRecorder = new LlmUsageRecorder();
12430
+ //#endregion
11740
12431
  //#region src/cli/commands/ncp/create-ui-ncp-agent.ts
11741
12432
  const CODEX_RUNTIME_KIND = "codex";
11742
12433
  const CODEX_DIRECT_RUNTIME_BACKEND = "codex-sdk";
@@ -11801,6 +12492,7 @@ async function createMcpRuntimeSupport(getConfig) {
11801
12492
  };
11802
12493
  }
11803
12494
  function createNativeRuntimeFactory(params, mcpToolRegistryAdapter, assetStore, sessionCreationService, sessionRequestBroker) {
12495
+ const observedProviderManager = new ObservedProviderManager(params.providerManager, new LlmUsageObserver(llmUsageRecorder, "ui-ncp"));
11804
12496
  return ({ stateManager, sessionMetadata, setSessionMetadata }) => {
11805
12497
  const reasoningNormalizationMode = resolveNativeReasoningNormalizationMode({
11806
12498
  config: params.getConfig(),
@@ -11809,7 +12501,7 @@ function createNativeRuntimeFactory(params, mcpToolRegistryAdapter, assetStore,
11809
12501
  if (reasoningNormalizationMode !== "off" && readAssistantReasoningNormalizationModeFromMetadata(sessionMetadata) !== reasoningNormalizationMode) setSessionMetadata(writeAssistantReasoningNormalizationModeToMetadata(sessionMetadata, reasoningNormalizationMode));
11810
12502
  const toolRegistry = new NextclawNcpToolRegistry({
11811
12503
  bus: params.bus,
11812
- providerManager: params.providerManager,
12504
+ providerManager: observedProviderManager,
11813
12505
  sessionManager: params.sessionManager,
11814
12506
  cronService: params.cronService,
11815
12507
  gatewayController: params.gatewayController,
@@ -11827,7 +12519,7 @@ function createNativeRuntimeFactory(params, mcpToolRegistryAdapter, assetStore,
11827
12519
  resolveMessageToolHints: params.resolveMessageToolHints,
11828
12520
  assetStore
11829
12521
  }),
11830
- llmApi: new ProviderManagerNcpLLMApi(params.providerManager),
12522
+ llmApi: new ProviderManagerNcpLLMApi(observedProviderManager),
11831
12523
  toolRegistry,
11832
12524
  stateManager,
11833
12525
  reasoningNormalizationMode
@@ -11905,7 +12597,10 @@ async function createUiNcpAgent(params) {
11905
12597
  onSessionRunStatusChanged: params.onSessionRunStatusChanged,
11906
12598
  createRuntime: (runtimeParams) => {
11907
12599
  pluginRuntimeRegistrationController.refreshPluginRuntimeRegistrations();
11908
- return runtimeRegistry.createRuntime(runtimeParams);
12600
+ return runtimeRegistry.createRuntime({
12601
+ ...runtimeParams,
12602
+ resolveAssetContentPath: (assetUri) => assetStore.resolveContentPath(assetUri)
12603
+ });
11909
12604
  }
11910
12605
  });
11911
12606
  await backend.start();
@@ -12372,430 +13067,92 @@ var ConfigReloader = class {
12372
13067
  }
12373
13068
  };
12374
13069
  //#endregion
12375
- //#region src/cli/commands/agent/agent-runtime-pool.ts
12376
- function normalizeAgentId(value) {
12377
- return (value ?? "").trim().toLowerCase() || "main";
13070
+ //#region src/cli/commands/service-support/gateway/service-cron-job-handler.ts
13071
+ function normalizeOptionalString$2(value) {
13072
+ if (typeof value !== "string") return;
13073
+ return value.trim() || void 0;
12378
13074
  }
12379
- function normalizeEngineKind(value) {
12380
- if (typeof value !== "string") return "native";
12381
- return value.trim().toLowerCase() || "native";
13075
+ function buildCronSessionMetadata(params) {
13076
+ const { job, agentId, accountId } = params;
13077
+ const channel = normalizeOptionalString$2(job.payload.channel) ?? "cli";
13078
+ const chatId = normalizeOptionalString$2(job.payload.to) ?? "direct";
13079
+ const metadata = {
13080
+ agentId,
13081
+ agent_id: agentId,
13082
+ channel,
13083
+ chatId,
13084
+ chat_id: chatId,
13085
+ label: job.name,
13086
+ cron_job_id: job.id,
13087
+ cron_job_name: job.name,
13088
+ session_origin: "cron"
13089
+ };
13090
+ if (accountId) {
13091
+ metadata.accountId = accountId;
13092
+ metadata.account_id = accountId;
13093
+ }
13094
+ return metadata;
12382
13095
  }
12383
- function toRecord(value) {
12384
- if (!value || typeof value !== "object" || Array.isArray(value)) return;
12385
- return value;
13096
+ function buildCronUserMessage(params) {
13097
+ const { sessionId, content, metadata } = params;
13098
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13099
+ return {
13100
+ id: `${sessionId}:user:cron:${timestamp}`,
13101
+ sessionId,
13102
+ role: "user",
13103
+ status: "final",
13104
+ timestamp,
13105
+ parts: [{
13106
+ type: "text",
13107
+ text: content
13108
+ }],
13109
+ metadata: structuredClone(metadata)
13110
+ };
12386
13111
  }
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
- }));
13112
+ function extractMessageText(message) {
13113
+ return message.parts.flatMap((part) => {
13114
+ if (part.type === "text" || part.type === "rich-text") return [part.text];
13115
+ return [];
13116
+ }).map((text) => text.trim()).filter((text) => text.length > 0).join("\n\n");
12406
13117
  }
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;
13118
+ async function runJobOverNcp(params) {
13119
+ const { agent, sessionId, message, metadata, missingCompletedMessageError, runErrorMessage } = params;
13120
+ let completedMessage;
13121
+ for await (const event of agent.runApi.send({
13122
+ sessionId,
13123
+ message,
13124
+ metadata
13125
+ })) {
13126
+ if (event.type === NcpEventType.MessageFailed) throw new Error(event.payload.error.message);
13127
+ if (event.type === NcpEventType.RunError) throw new Error(event.payload.error ?? runErrorMessage);
13128
+ if (event.type === NcpEventType.MessageCompleted) completedMessage = event.payload.message;
12619
13129
  }
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;
12766
- }
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
- };
13130
+ if (!completedMessage) throw new Error(missingCompletedMessageError);
13131
+ return extractMessageText(completedMessage);
12787
13132
  }
12788
13133
  function createCronJobHandler(params) {
12789
13134
  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",
13135
+ const ncpAgent = params.resolveNcpAgent();
13136
+ if (!ncpAgent) throw new Error("NCP agent is not ready for cron execution.");
13137
+ const accountId = normalizeOptionalString$2(job.payload.accountId);
13138
+ const agentId = normalizeOptionalString$2(job.payload.agentId) ?? "main";
13139
+ const sessionId = `cron:${job.id}`;
13140
+ const metadata = buildCronSessionMetadata({
13141
+ job,
13142
+ agentId,
13143
+ accountId
13144
+ });
13145
+ const response = await runJobOverNcp({
13146
+ agent: ncpAgent,
13147
+ sessionId,
13148
+ message: buildCronUserMessage({
13149
+ sessionId,
13150
+ content: job.payload.message,
13151
+ metadata
13152
+ }),
12797
13153
  metadata,
12798
- agentId
13154
+ missingCompletedMessageError: "cron job completed without a final assistant message",
13155
+ runErrorMessage: "cron job failed"
12799
13156
  });
12800
13157
  if (job.payload.deliver && job.payload.to) await params.bus.publishOutbound({
12801
13158
  channel: job.payload.channel ?? "cli",
@@ -12807,21 +13164,81 @@ function createCronJobHandler(params) {
12807
13164
  return response;
12808
13165
  };
12809
13166
  }
13167
+ function buildHeartbeatMetadata(params) {
13168
+ return {
13169
+ agentId: params.agentId,
13170
+ agent_id: params.agentId,
13171
+ channel: "cli",
13172
+ chatId: "direct",
13173
+ chat_id: "direct",
13174
+ label: "heartbeat",
13175
+ session_origin: "heartbeat"
13176
+ };
13177
+ }
13178
+ function buildHeartbeatUserMessage(params) {
13179
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13180
+ return {
13181
+ id: `heartbeat:user:${timestamp}`,
13182
+ sessionId: "heartbeat",
13183
+ role: "user",
13184
+ status: "final",
13185
+ timestamp,
13186
+ parts: [{
13187
+ type: "text",
13188
+ text: params.content
13189
+ }],
13190
+ metadata: structuredClone(params.metadata)
13191
+ };
13192
+ }
13193
+ function createHeartbeatJobHandler(params) {
13194
+ return async (prompt) => {
13195
+ const ncpAgent = params.resolveNcpAgent();
13196
+ if (!ncpAgent) throw new Error("NCP agent is not ready for heartbeat execution.");
13197
+ const metadata = buildHeartbeatMetadata({ agentId: params.resolveAgentId() });
13198
+ return await runJobOverNcp({
13199
+ agent: ncpAgent,
13200
+ sessionId: "heartbeat",
13201
+ message: buildHeartbeatUserMessage({
13202
+ content: prompt,
13203
+ metadata
13204
+ }),
13205
+ metadata,
13206
+ missingCompletedMessageError: "heartbeat execution completed without a final assistant message",
13207
+ runErrorMessage: "heartbeat execution failed"
13208
+ });
13209
+ };
13210
+ }
12810
13211
  //#endregion
12811
13212
  //#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;
13213
+ 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
13214
  function applyGatewayCapabilityState(gateway, next) {
12814
13215
  gateway.pluginRegistry = next.pluginRegistry;
12815
13216
  gateway.pluginChannelBindings = next.pluginChannelBindings;
12816
13217
  gateway.extensionRegistry = next.extensionRegistry;
12817
13218
  }
13219
+ function normalizeAgentId(value) {
13220
+ return (value ?? "").trim().toLowerCase() || "main";
13221
+ }
13222
+ function createGatewayHeartbeat(state, params) {
13223
+ const handleHeartbeat = createHeartbeatJobHandler({
13224
+ resolveNcpAgent: () => params.getLiveUiNcpAgent?.() ?? null,
13225
+ resolveAgentId: () => normalizeAgentId(resolveDefaultAgentProfileId$1(resolveConfigSecrets$3(loadConfig$3(), { configPath: state.runtimeConfigPath })))
13226
+ });
13227
+ return new HeartbeatService(state.workspace, async (promptText) => await handleHeartbeat(promptText), 1800, true);
13228
+ }
13229
+ function createGatewayCronJobHandler(params) {
13230
+ return createCronJobHandler({
13231
+ resolveNcpAgent: () => params.getLiveUiNcpAgent?.() ?? null,
13232
+ bus: params.bus
13233
+ });
13234
+ }
12818
13235
  function createGatewayShellContext(params) {
12819
13236
  const runtimeConfigPath = getConfigPath$2();
12820
13237
  const config = resolveConfigSecrets$3(loadConfig$3(), { configPath: runtimeConfigPath });
12821
13238
  const workspace = getWorkspacePath$2(config.agents.defaults.workspace);
12822
13239
  const homeDir = getDataDir$1();
12823
13240
  const cronStorePath = join(getDataDir$1(), "cron", "jobs.json");
12824
- const sessionManager = measureStartupSync("service.gateway_shell_context.session_manager", () => new SessionManager$1({
13241
+ const sessionManager = measureStartupSync("service.gateway_shell_context.session_manager", () => new SessionManager$2({
12825
13242
  workspace,
12826
13243
  homeDir
12827
13244
  }));
@@ -12842,10 +13259,11 @@ function createGatewayShellContext(params) {
12842
13259
  };
12843
13260
  }
12844
13261
  function createGatewayStartupContext(params) {
13262
+ const { shellContext: providedShellContext, uiOverrides, allowMissingProvider, uiStaticDir, initialPluginRegistry, makeProvider, makeMissingProvider, requestRestart, getLiveUiNcpAgent } = params;
12845
13263
  const state = {};
12846
- const shellContext = params.shellContext ?? createGatewayShellContext({
12847
- uiOverrides: params.uiOverrides,
12848
- uiStaticDir: params.uiStaticDir
13264
+ const shellContext = providedShellContext ?? createGatewayShellContext({
13265
+ uiOverrides,
13266
+ uiStaticDir
12849
13267
  });
12850
13268
  state.runtimeConfigPath = shellContext.runtimeConfigPath;
12851
13269
  state.config = shellContext.config;
@@ -12855,14 +13273,14 @@ function createGatewayStartupContext(params) {
12855
13273
  state.uiConfig = shellContext.uiConfig;
12856
13274
  state.uiStaticDir = shellContext.uiStaticDir;
12857
13275
  state.remoteModule = shellContext.remoteModule;
12858
- state.pluginRegistry = params.initialPluginRegistry ?? measureStartupSync("service.gateway_context.load_plugin_registry", () => loadPluginRegistry(state.config, state.workspace));
13276
+ state.pluginRegistry = initialPluginRegistry ?? measureStartupSync("service.gateway_context.load_plugin_registry", () => loadPluginRegistry(state.config, state.workspace));
12859
13277
  state.pluginChannelBindings = measureStartupSync("service.gateway_context.get_plugin_channel_bindings", () => getPluginChannelBindings(state.pluginRegistry));
12860
13278
  state.extensionRegistry = measureStartupSync("service.gateway_context.to_extension_registry", () => toExtensionRegistry(state.pluginRegistry));
12861
13279
  logPluginDiagnostics(state.pluginRegistry);
12862
13280
  state.bus = new MessageBus$2();
12863
- const provider = params.allowMissingProvider === true ? params.makeProvider(state.config, { allowMissing: true }) : params.makeProvider(state.config);
13281
+ const provider = allowMissingProvider === true ? makeProvider(state.config, { allowMissing: true }) : makeProvider(state.config);
12864
13282
  state.providerManager = measureStartupSync("service.gateway_context.provider_manager", () => new ProviderManager$1({
12865
- defaultProvider: provider ?? params.makeMissingProvider(state.config),
13283
+ defaultProvider: provider ?? makeMissingProvider(state.config),
12866
13284
  config: state.config
12867
13285
  }));
12868
13286
  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 +13291,12 @@ function createGatewayStartupContext(params) {
12873
13291
  bus: state.bus,
12874
13292
  sessionManager: state.sessionManager,
12875
13293
  providerManager: state.providerManager,
12876
- makeProvider: (nextConfig) => params.makeProvider(nextConfig, { allowMissing: true }) ?? params.makeMissingProvider(nextConfig),
13294
+ makeProvider: (nextConfig) => makeProvider(nextConfig, { allowMissing: true }) ?? makeMissingProvider(nextConfig),
12877
13295
  loadConfig: () => resolveConfigSecrets$3(loadConfig$3(), { configPath: state.runtimeConfigPath }),
12878
13296
  resolveChannelConfig: (nextConfig) => resolveChannelConfigView(nextConfig, state.pluginChannelBindings),
12879
13297
  getExtensionChannels: () => state.extensionRegistry.channels,
12880
13298
  onRestartRequired: (paths) => {
12881
- params.requestRestart({
13299
+ requestRestart({
12882
13300
  reason: `config reload requires restart: ${paths.join(", ")}`,
12883
13301
  manualMessage: `Config changes require restart: ${paths.join(", ")}`,
12884
13302
  strategy: "background-service-or-manual"
@@ -12895,7 +13313,7 @@ function createGatewayStartupContext(params) {
12895
13313
  getConfigPath: getConfigPath$2,
12896
13314
  saveConfig: saveConfig$1,
12897
13315
  requestRestart: async (options) => {
12898
- await params.requestRestart({
13316
+ await requestRestart({
12899
13317
  reason: options?.reason ?? "gateway tool restart",
12900
13318
  manualMessage: "Restart the gateway to apply changes.",
12901
13319
  strategy: "background-service-or-exit",
@@ -12904,37 +13322,356 @@ function createGatewayStartupContext(params) {
12904
13322
  });
12905
13323
  }
12906
13324
  }));
12907
- state.runtimePool = measureStartupSync("service.gateway_context.runtime_pool", () => new GatewayAgentRuntimePool({
13325
+ state.cron.onJob = createGatewayCronJobHandler({
12908
13326
  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
13327
+ getLiveUiNcpAgent
12929
13328
  });
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);
13329
+ state.heartbeat = createGatewayHeartbeat(state, { getLiveUiNcpAgent });
12935
13330
  return state;
12936
13331
  }
12937
13332
  //#endregion
13333
+ //#region src/cli/commands/ncp/runtime/nextclaw-ncp-runner.ts
13334
+ function normalizeOptionalString$1(value) {
13335
+ if (typeof value !== "string") return;
13336
+ return value.trim() || void 0;
13337
+ }
13338
+ function resolveAttachmentName(attachment) {
13339
+ const explicitName = normalizeOptionalString$1(attachment.name);
13340
+ if (explicitName) return explicitName;
13341
+ const explicitPath = normalizeOptionalString$1(attachment.path);
13342
+ if (explicitPath) return basename(explicitPath);
13343
+ const explicitUrl = normalizeOptionalString$1(attachment.url);
13344
+ if (explicitUrl) try {
13345
+ return basename(new URL(explicitUrl).pathname) || "asset.bin";
13346
+ } catch {
13347
+ return basename(explicitUrl) || "asset.bin";
13348
+ }
13349
+ return "asset.bin";
13350
+ }
13351
+ async function attachmentToPart(attachment, assetApi) {
13352
+ const assetUri = normalizeOptionalString$1(attachment.assetUri);
13353
+ if (assetUri) return createFilePartFromAssetUri(attachment, assetUri);
13354
+ const remoteUrl = normalizeOptionalString$1(attachment.url);
13355
+ const localPath = normalizeOptionalString$1(attachment.path);
13356
+ if (localPath) return await createFilePartFromLocalPath(attachment, localPath, assetApi);
13357
+ if (remoteUrl) return createFilePartFromRemoteUrl(attachment, remoteUrl);
13358
+ throw new Error(`Unsupported attachment payload for "${resolveAttachmentName(attachment)}".`);
13359
+ }
13360
+ function createBaseFilePart(attachment) {
13361
+ return {
13362
+ ...attachment.name ? { name: attachment.name } : {},
13363
+ ...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
13364
+ ...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
13365
+ };
13366
+ }
13367
+ function createFilePartFromAssetUri(attachment, assetUri) {
13368
+ return {
13369
+ type: "file",
13370
+ assetUri,
13371
+ ...createBaseFilePart(attachment)
13372
+ };
13373
+ }
13374
+ async function createFilePartFromLocalPath(attachment, localPath, assetApi) {
13375
+ if (!assetApi) throw new Error("NCP asset api is unavailable for local attachments.");
13376
+ const fileName = resolveAttachmentName(attachment);
13377
+ const bytes = await readFile(localPath);
13378
+ return {
13379
+ type: "file",
13380
+ assetUri: (await assetApi.put({
13381
+ fileName,
13382
+ mimeType: attachment.mimeType ?? null,
13383
+ bytes
13384
+ })).uri,
13385
+ name: attachment.name ?? fileName,
13386
+ ...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
13387
+ ...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
13388
+ };
13389
+ }
13390
+ function createFilePartFromRemoteUrl(attachment, remoteUrl) {
13391
+ return {
13392
+ type: "file",
13393
+ url: remoteUrl,
13394
+ name: attachment.name ?? resolveAttachmentName(attachment),
13395
+ ...attachment.mimeType ? { mimeType: attachment.mimeType } : {},
13396
+ ...typeof attachment.size === "number" ? { sizeBytes: attachment.size } : {}
13397
+ };
13398
+ }
13399
+ async function buildUserMessageParts(params) {
13400
+ const parts = [];
13401
+ if (params.content.length > 0) parts.push({
13402
+ type: "text",
13403
+ text: params.content
13404
+ });
13405
+ for (const attachment of params.attachments ?? []) parts.push(await attachmentToPart(attachment, params.assetApi));
13406
+ return parts;
13407
+ }
13408
+ async function buildNcpUserMessage(params) {
13409
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
13410
+ return {
13411
+ id: `${params.sessionId}:user:${timestamp}`,
13412
+ sessionId: params.sessionId,
13413
+ role: "user",
13414
+ status: "final",
13415
+ timestamp,
13416
+ parts: await buildUserMessageParts({
13417
+ content: params.content,
13418
+ attachments: params.attachments,
13419
+ assetApi: params.assetApi
13420
+ }),
13421
+ metadata: structuredClone(params.metadata ?? {})
13422
+ };
13423
+ }
13424
+ async function runPromptOverNcp(params) {
13425
+ const message = await buildNcpUserMessage({
13426
+ sessionId: params.sessionId,
13427
+ content: params.content,
13428
+ attachments: params.attachments,
13429
+ metadata: params.metadata,
13430
+ assetApi: params.agent.assetApi
13431
+ });
13432
+ let completedMessage;
13433
+ for await (const event of params.agent.runApi.send({
13434
+ sessionId: params.sessionId,
13435
+ message,
13436
+ metadata: params.metadata
13437
+ }, { ...params.abortSignal ? { signal: params.abortSignal } : {} })) {
13438
+ params.onEvent?.(event);
13439
+ if (event.type === NcpEventType.MessageTextDelta) {
13440
+ params.onAssistantDelta?.(event.payload.delta);
13441
+ continue;
13442
+ }
13443
+ if (event.type === NcpEventType.MessageFailed) throw new Error(event.payload.error.message);
13444
+ if (event.type === NcpEventType.RunError) throw new Error(event.payload.error ?? params.runErrorMessage ?? "NCP run failed.");
13445
+ if (event.type === NcpEventType.MessageCompleted) completedMessage = event.payload.message;
13446
+ }
13447
+ if (!completedMessage) throw new Error(params.missingCompletedMessageError ?? "NCP run completed without a final assistant message.");
13448
+ return {
13449
+ text: extractTextFromNcpMessage(completedMessage),
13450
+ completedMessage
13451
+ };
13452
+ }
13453
+ //#endregion
13454
+ //#region src/cli/commands/ncp/runtime/nextclaw-ncp-dispatch.ts
13455
+ function normalizeOptionalString(value) {
13456
+ if (typeof value !== "string") return;
13457
+ return value.trim() || void 0;
13458
+ }
13459
+ function requireNcpAgent(resolveNcpAgent, purpose) {
13460
+ const agent = resolveNcpAgent?.() ?? null;
13461
+ if (!agent) throw new Error(`NCP agent is not ready for ${purpose}.`);
13462
+ return agent;
13463
+ }
13464
+ function createDirectInboundMessage(params) {
13465
+ return {
13466
+ channel: params.channel ?? "cli",
13467
+ senderId: "user",
13468
+ chatId: params.chatId ?? "direct",
13469
+ content: params.content,
13470
+ timestamp: /* @__PURE__ */ new Date(),
13471
+ attachments: params.attachments ?? [],
13472
+ metadata: structuredClone(params.metadata ?? {})
13473
+ };
13474
+ }
13475
+ function buildRunMetadata(params) {
13476
+ return {
13477
+ ...params.message.metadata ?? {},
13478
+ ...params.metadata ?? {},
13479
+ channel: params.message.channel,
13480
+ chatId: params.message.chatId,
13481
+ chat_id: params.message.chatId,
13482
+ accountId: params.route.accountId,
13483
+ account_id: params.route.accountId,
13484
+ agentId: params.route.agentId,
13485
+ agent_id: params.route.agentId,
13486
+ sessionKey: params.route.sessionKey,
13487
+ session_key: params.route.sessionKey,
13488
+ senderId: params.message.senderId,
13489
+ sender_id: params.message.senderId
13490
+ };
13491
+ }
13492
+ function parseCommandOptionValue(type, rawValue) {
13493
+ const value = rawValue.trim();
13494
+ if (!value) return;
13495
+ if (type === "number") {
13496
+ const parsed = Number(value);
13497
+ return Number.isFinite(parsed) ? parsed : void 0;
13498
+ }
13499
+ if (type === "boolean") {
13500
+ const lowered = value.toLowerCase();
13501
+ if ([
13502
+ "1",
13503
+ "true",
13504
+ "yes",
13505
+ "on"
13506
+ ].includes(lowered)) return true;
13507
+ if ([
13508
+ "0",
13509
+ "false",
13510
+ "no",
13511
+ "off"
13512
+ ].includes(lowered)) return false;
13513
+ return;
13514
+ }
13515
+ return value;
13516
+ }
13517
+ function parseCommandArgsFromText(commandName, rawTail, specs) {
13518
+ if (!rawTail) return {};
13519
+ const options = specs.find((item) => item.name.trim().toLowerCase() === commandName)?.options;
13520
+ if (!options || options.length === 0) return {};
13521
+ const tokens = rawTail.split(/\s+/).filter(Boolean);
13522
+ const args = {};
13523
+ let cursor = 0;
13524
+ for (let index = 0; index < options.length; index += 1) {
13525
+ if (cursor >= tokens.length) break;
13526
+ const option = options[index];
13527
+ const isLastOption = index === options.length - 1;
13528
+ const rawValue = isLastOption ? tokens.slice(cursor).join(" ") : tokens[cursor];
13529
+ cursor += isLastOption ? tokens.length - cursor : 1;
13530
+ const parsedValue = parseCommandOptionValue(option.type, rawValue);
13531
+ if (parsedValue !== void 0) args[option.name] = parsedValue;
13532
+ }
13533
+ return args;
13534
+ }
13535
+ async function executeSlashCommandMaybe(params) {
13536
+ const trimmed = params.rawContent.trim();
13537
+ if (!trimmed.startsWith("/")) return null;
13538
+ const registry = new CommandRegistry(params.config, params.sessionManager);
13539
+ const executeText = registry.executeText;
13540
+ if (typeof executeText === "function") return (await executeText.call(registry, params.rawContent, {
13541
+ channel: params.channel,
13542
+ chatId: params.chatId,
13543
+ senderId: "user",
13544
+ sessionKey: params.sessionKey
13545
+ }))?.content ?? null;
13546
+ const commandRaw = trimmed.slice(1).trim();
13547
+ if (!commandRaw) return null;
13548
+ const [nameToken, ...restTokens] = commandRaw.split(/\s+/);
13549
+ const commandName = nameToken.trim().toLowerCase();
13550
+ if (!commandName) return null;
13551
+ const args = parseCommandArgsFromText(commandName, restTokens.join(" ").trim(), registry.listSlashCommands());
13552
+ return (await registry.execute(commandName, args, {
13553
+ channel: params.channel,
13554
+ chatId: params.chatId,
13555
+ senderId: "user",
13556
+ sessionKey: params.sessionKey
13557
+ }))?.content ?? null;
13558
+ }
13559
+ function resolveDirectRoute(params) {
13560
+ const message = createDirectInboundMessage(params);
13561
+ const forcedAgentId = normalizeOptionalString(params.agentId) ?? parseAgentScopedSessionKey(params.sessionKey)?.agentId ?? void 0;
13562
+ return {
13563
+ message,
13564
+ route: new AgentRouteResolver(params.config).resolveInbound({
13565
+ message,
13566
+ forcedAgentId,
13567
+ sessionKeyOverride: params.sessionKey
13568
+ })
13569
+ };
13570
+ }
13571
+ function formatUserFacingError(error, maxChars = 320) {
13572
+ const normalized = (error instanceof Error ? error.message || error.name || "Unknown error" : String(error ?? "Unknown error")).replace(/\s+/g, " ").trim();
13573
+ if (!normalized) return "Unknown error";
13574
+ if (normalized.length <= maxChars) return normalized;
13575
+ return `${normalized.slice(0, Math.max(0, maxChars - 3)).trimEnd()}...`;
13576
+ }
13577
+ async function dispatchPromptOverNcp(params) {
13578
+ const { message, route } = resolveDirectRoute({
13579
+ config: params.config,
13580
+ content: params.content,
13581
+ sessionKey: params.sessionKey,
13582
+ channel: params.channel,
13583
+ chatId: params.chatId,
13584
+ attachments: params.attachments,
13585
+ metadata: params.metadata,
13586
+ agentId: params.agentId
13587
+ });
13588
+ const commandResult = await executeSlashCommandMaybe({
13589
+ config: params.config,
13590
+ sessionManager: params.sessionManager,
13591
+ rawContent: params.content,
13592
+ channel: message.channel,
13593
+ chatId: message.chatId,
13594
+ sessionKey: route.sessionKey
13595
+ });
13596
+ if (commandResult) return commandResult;
13597
+ return (await runPromptOverNcp({
13598
+ agent: requireNcpAgent(params.resolveNcpAgent, "direct dispatch"),
13599
+ sessionId: route.sessionKey,
13600
+ content: params.content,
13601
+ attachments: params.attachments,
13602
+ metadata: buildRunMetadata({
13603
+ message,
13604
+ route
13605
+ }),
13606
+ abortSignal: params.abortSignal,
13607
+ onAssistantDelta: params.onAssistantDelta,
13608
+ missingCompletedMessageError: `session "${route.sessionKey}" completed without a final assistant message`,
13609
+ runErrorMessage: `session "${route.sessionKey}" failed`
13610
+ })).text;
13611
+ }
13612
+ async function runGatewayInboundLoop(params) {
13613
+ while (true) {
13614
+ const message = await params.bus.consumeInbound();
13615
+ try {
13616
+ const explicitSessionKey = normalizeOptionalString(message.metadata.session_key_override);
13617
+ const forcedAgentId = normalizeOptionalString(message.metadata.target_agent_id);
13618
+ const route = new AgentRouteResolver(params.getConfig()).resolveInbound({
13619
+ message,
13620
+ forcedAgentId,
13621
+ sessionKeyOverride: explicitSessionKey
13622
+ });
13623
+ const agent = requireNcpAgent(params.resolveNcpAgent, "gateway dispatch");
13624
+ if (message.channel !== "system") await params.bus.publishOutbound(createAssistantStreamResetControlMessage(message));
13625
+ const result = await runPromptOverNcp({
13626
+ agent,
13627
+ sessionId: route.sessionKey,
13628
+ content: message.content,
13629
+ attachments: message.attachments,
13630
+ metadata: buildRunMetadata({
13631
+ message,
13632
+ route
13633
+ }),
13634
+ onAssistantDelta: message.channel !== "system" ? (delta) => {
13635
+ if (!delta) return;
13636
+ params.bus.publishOutbound(createAssistantStreamDeltaControlMessage(message, delta));
13637
+ } : void 0,
13638
+ missingCompletedMessageError: `session "${route.sessionKey}" completed without a final assistant message`,
13639
+ runErrorMessage: `session "${route.sessionKey}" failed`
13640
+ });
13641
+ if (message.channel === "system") {
13642
+ params.onSystemSessionUpdated?.({
13643
+ sessionKey: route.sessionKey,
13644
+ message
13645
+ });
13646
+ continue;
13647
+ }
13648
+ if (!result.text.trim()) {
13649
+ await params.bus.publishOutbound(createTypingStopControlMessage(message));
13650
+ continue;
13651
+ }
13652
+ await params.bus.publishOutbound({
13653
+ channel: message.channel,
13654
+ chatId: message.chatId,
13655
+ content: result.text,
13656
+ media: [],
13657
+ metadata: buildRunMetadata({
13658
+ message,
13659
+ route,
13660
+ metadata: result.completedMessage.metadata
13661
+ })
13662
+ });
13663
+ } catch (error) {
13664
+ await params.bus.publishOutbound({
13665
+ channel: message.channel,
13666
+ chatId: message.chatId,
13667
+ content: `Sorry, I encountered an error: ${formatUserFacingError(error)}`,
13668
+ media: [],
13669
+ metadata: {}
13670
+ });
13671
+ }
13672
+ }
13673
+ }
13674
+ //#endregion
12938
13675
  //#region src/cli/commands/service-support/session/service-deferred-ncp-agent.ts
12939
13676
  const DEFAULT_BASE_PATH = "/api/ncp/agent";
12940
13677
  const DEFERRED_NCP_AGENT_UNAVAILABLE = "ncp agent unavailable during startup";
@@ -13020,13 +13757,13 @@ function createDeferredUiNcpAgent(basePath = DEFAULT_BASE_PATH) {
13020
13757
  }
13021
13758
  //#endregion
13022
13759
  //#region src/cli/commands/service-support/gateway/service-gateway-startup.ts
13023
- function wireSystemSessionUpdatedPublisher(params) {
13024
- params.runtimePool.setSystemSessionUpdatedHandler(({ sessionKey }) => {
13760
+ function createSystemSessionUpdatedPublisher(params) {
13761
+ return ({ sessionKey }) => {
13025
13762
  params.publishUiEvent?.({
13026
13763
  type: "session.updated",
13027
13764
  payload: { sessionKey }
13028
13765
  });
13029
- });
13766
+ };
13030
13767
  }
13031
13768
  async function startUiShell(params) {
13032
13769
  logStartupTrace("service.start_ui_shell.begin");
@@ -13067,54 +13804,90 @@ async function startUiShell(params) {
13067
13804
  };
13068
13805
  }
13069
13806
  async function startDeferredGatewayStartup(params) {
13807
+ const { uiStartup, deferredNcpSessionService, bus, sessionManager, providerManager, cronService, gatewayController, getConfig, getExtensionRegistry, resolveMessageToolHints, hydrateCapabilities, startPluginGateways, startChannels, wakeFromRestartSentinel, onNcpAgentReady, publishSessionChange } = params;
13070
13808
  logStartupTrace("service.deferred_startup.begin");
13071
- if (params.uiStartup) try {
13809
+ try {
13072
13810
  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,
13811
+ bus,
13812
+ providerManager,
13813
+ sessionManager,
13814
+ cronService,
13815
+ gatewayController,
13816
+ getConfig,
13817
+ getExtensionRegistry,
13818
+ onSessionUpdated: publishSessionChange,
13081
13819
  onSessionRunStatusChanged: (payload) => {
13082
- params.uiStartup?.publish({
13820
+ uiStartup?.publish({
13083
13821
  type: "session.run-status",
13084
13822
  payload
13085
13823
  });
13086
13824
  },
13087
- resolveMessageToolHints: ({ channel, accountId }) => params.resolveMessageToolHints({
13825
+ resolveMessageToolHints: ({ channel, accountId }) => resolveMessageToolHints({
13088
13826
  channel,
13089
13827
  accountId
13090
13828
  })
13091
13829
  }));
13092
- params.deferredNcpSessionService.activate(ncpAgent.sessionApi);
13093
- params.onNcpAgentReady(ncpAgent);
13094
- params.uiStartup.deferredNcpAgent.activate(ncpAgent);
13095
- console.log("✓ UI NCP agent: ready");
13830
+ deferredNcpSessionService.activate(ncpAgent.sessionApi);
13831
+ onNcpAgentReady(ncpAgent);
13832
+ if (uiStartup) {
13833
+ uiStartup.deferredNcpAgent.activate(ncpAgent);
13834
+ console.log("✓ UI NCP agent: ready");
13835
+ } else console.log("✓ Service NCP agent: ready");
13096
13836
  } catch (error) {
13097
13837
  console.error(`UI NCP agent startup failed: ${error instanceof Error ? error.message : String(error)}`);
13098
13838
  }
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);
13839
+ if (hydrateCapabilities) await measureStartupAsync("service.deferred_startup.hydrate_capabilities", hydrateCapabilities);
13840
+ await measureStartupAsync("service.deferred_startup.start_plugin_gateways", startPluginGateways);
13841
+ await measureStartupAsync("service.deferred_startup.start_channels", startChannels);
13842
+ await measureStartupAsync("service.deferred_startup.wake_restart_sentinel", wakeFromRestartSentinel);
13103
13843
  console.log("✓ Deferred startup: plugin gateways and channels settled");
13104
13844
  logStartupTrace("service.deferred_startup.end");
13105
13845
  }
13106
13846
  async function runGatewayRuntimeLoop(params) {
13107
13847
  let startupTask = null;
13108
13848
  try {
13109
- const runtimePoolTask = params.runtimePool.run();
13849
+ const runtimeLoopTask = params.runRuntimeLoop();
13110
13850
  startupTask = params.startDeferredStartup();
13111
13851
  startupTask.catch(params.onDeferredStartupError);
13112
- await runtimePoolTask;
13852
+ await runtimeLoopTask;
13113
13853
  } finally {
13114
13854
  if (startupTask) await startupTask.catch(() => void 0);
13115
13855
  await params.cleanup();
13116
13856
  }
13117
13857
  }
13858
+ async function runConfiguredGatewayRuntime(params) {
13859
+ const onSystemSessionUpdated = createSystemSessionUpdatedPublisher({ publishUiEvent: params.publishUiEvent });
13860
+ logStartupTrace("service.start_gateway.runtime_loop_begin");
13861
+ await runGatewayRuntimeLoop({
13862
+ runRuntimeLoop: () => runGatewayInboundLoop({
13863
+ bus: params.gateway.bus,
13864
+ sessionManager: params.gateway.sessionManager,
13865
+ getConfig: params.getConfig,
13866
+ resolveNcpAgent: params.getLiveUiNcpAgent,
13867
+ onSystemSessionUpdated: ({ sessionKey }) => onSystemSessionUpdated({ sessionKey })
13868
+ }),
13869
+ startDeferredStartup: () => startDeferredGatewayStartup({
13870
+ uiStartup: params.uiStartup,
13871
+ deferredNcpSessionService: params.deferredNcpSessionService,
13872
+ bus: params.gateway.bus,
13873
+ sessionManager: params.gateway.sessionManager,
13874
+ providerManager: params.gateway.providerManager,
13875
+ cronService: params.gateway.cron,
13876
+ gatewayController: params.gateway.gatewayController,
13877
+ getConfig: params.getConfig,
13878
+ getExtensionRegistry: params.getExtensionRegistry,
13879
+ resolveMessageToolHints: params.resolveMessageToolHints,
13880
+ hydrateCapabilities: params.deferredStartupHooks.hydrateCapabilities,
13881
+ startPluginGateways: params.deferredStartupHooks.startPluginGateways,
13882
+ startChannels: params.deferredStartupHooks.startChannels,
13883
+ wakeFromRestartSentinel: params.deferredStartupHooks.wakeFromRestartSentinel,
13884
+ onNcpAgentReady: params.deferredStartupHooks.onNcpAgentReady,
13885
+ publishSessionChange: params.publishSessionChange
13886
+ }),
13887
+ onDeferredStartupError: params.onDeferredStartupError,
13888
+ cleanup: params.cleanup
13889
+ });
13890
+ }
13118
13891
  //#endregion
13119
13892
  //#region src/cli/commands/ncp/session/ncp-session-realtime-change.ts
13120
13893
  function toNcpSessionRealtimeEvent(change) {
@@ -13242,6 +14015,10 @@ function createDeferredUiNcpSessionService(fallbackService) {
13242
14015
  }
13243
14016
  //#endregion
13244
14017
  //#region src/cli/commands/service-support/session/service-ncp-session-realtime-bridge.ts
14018
+ function formatBackgroundTaskError(error) {
14019
+ if (error instanceof Error) return error.stack ?? error.message;
14020
+ return String(error);
14021
+ }
13245
14022
  function createLatestOnlySessionChangePublisher(publishSessionChange) {
13246
14023
  const inFlightTasks = /* @__PURE__ */ new Map();
13247
14024
  const rerunKeys = /* @__PURE__ */ new Set();
@@ -13272,7 +14049,9 @@ function createServiceNcpSessionRealtimeBridge(params) {
13272
14049
  let publishSessionChange = async (_sessionKey) => {};
13273
14050
  let scheduleSessionChange = async (_sessionKey) => {};
13274
14051
  const deferredSessionService = createDeferredUiNcpSessionService(new UiSessionService(params.sessionManager, { onSessionUpdated: (sessionKey) => {
13275
- scheduleSessionChange(sessionKey);
14052
+ scheduleSessionChange(sessionKey).catch((error) => {
14053
+ console.error(`[session-realtime] failed to publish session change for ${sessionKey}: ${formatBackgroundTaskError(error)}`);
14054
+ });
13276
14055
  } }));
13277
14056
  const publishLatestSessionChange = async (sessionKey) => {
13278
14057
  await createNcpSessionRealtimeChangePublisher({
@@ -13297,35 +14076,26 @@ function createServiceNcpSessionRealtimeBridge(params) {
13297
14076
  //#endregion
13298
14077
  //#region src/cli/commands/plugin/plugin-registry-loader.ts
13299
14078
  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
- };
14079
+ return getAppLogger("plugin.registry_loader");
13306
14080
  }
13307
14081
  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
- };
14082
+ return resolveDevPluginLoadingContext(config, resolveDevFirstPartyPluginDir(process.env.NEXTCLAW_DEV_FIRST_PARTY_PLUGIN_DIR));
13313
14083
  }
13314
14084
  async function loadPluginRegistryProgressively(config, workspaceDir, options = {}) {
13315
- const { workspaceExtensionsDir, configWithDevPluginPaths } = withDevFirstPartyPluginPaths(config);
14085
+ const { configWithDevPluginOverrides, excludedRoots } = withDevFirstPartyPluginPaths(config);
13316
14086
  return await loadOpenClawPluginsProgressively({
13317
- config: configWithDevPluginPaths,
14087
+ config: configWithDevPluginOverrides,
13318
14088
  workspaceDir,
13319
- excludeRoots: resolveDevFirstPartyPluginInstallRoots(config, workspaceExtensionsDir),
14089
+ excludeRoots: excludedRoots,
13320
14090
  ...buildReservedPluginLoadOptions(),
13321
14091
  onPluginProcessed: options.onPluginProcessed,
13322
14092
  logger: createPluginLogger()
13323
14093
  });
13324
14094
  }
13325
14095
  function discoverPluginRegistryStatus(config, workspaceDir) {
13326
- const { configWithDevPluginPaths } = withDevFirstPartyPluginPaths(config);
14096
+ const { configWithDevPluginOverrides } = withDevFirstPartyPluginPaths(config);
13327
14097
  return discoverPluginStatusReport({
13328
- config: configWithDevPluginPaths,
14098
+ config: configWithDevPluginOverrides,
13329
14099
  workspaceDir
13330
14100
  });
13331
14101
  }
@@ -13335,7 +14105,6 @@ function createEmptyPluginRegistry() {
13335
14105
  tools: [],
13336
14106
  channels: [],
13337
14107
  providers: [],
13338
- engines: [],
13339
14108
  ncpAgentRuntimes: [],
13340
14109
  diagnostics: [],
13341
14110
  resolvedTools: []
@@ -13554,8 +14323,6 @@ async function hydrateServiceCapabilities(params) {
13554
14323
  params.state.extensionRegistry = nextExtensionRegistry;
13555
14324
  params.state.pluginChannelBindings = nextPluginChannelBindings;
13556
14325
  params.state.pluginUiMetadata = nextPluginUiMetadata;
13557
- params.gateway.runtimePool.applyExtensionRegistry(nextExtensionRegistry);
13558
- params.gateway.runtimePool.applyRuntimeConfig(nextConfig);
13559
14326
  params.getLiveUiNcpAgent()?.applyExtensionRegistry?.(nextExtensionRegistry);
13560
14327
  if (shouldRebuildChannels) await params.gateway.reloader.rebuildChannels(nextConfig, { start: false });
13561
14328
  params.uiStartup?.publish({
@@ -13579,7 +14346,7 @@ async function hydrateServiceCapabilities(params) {
13579
14346
  //#endregion
13580
14347
  //#region src/cli/commands/service-support/plugin/service-plugin-runtime-bridge.ts
13581
14348
  function installPluginRuntimeBridge(params) {
13582
- const { runtimePool, runtimeConfigPath, getPluginChannelBindings } = params;
14349
+ const { dispatchPrompt, runtimeConfigPath, getPluginChannelBindings } = params;
13583
14350
  setPluginRuntimeBridge({
13584
14351
  loadConfig: () => toPluginConfigView$1(resolveConfigSecrets(loadConfig(), { configPath: runtimeConfigPath }), getPluginChannelBindings()),
13585
14352
  writeConfigFile: async (nextConfigView) => {
@@ -13591,7 +14358,7 @@ function installPluginRuntimeBridge(params) {
13591
14358
  if (!request) return;
13592
14359
  try {
13593
14360
  await dispatcherOptions.onReplyStart?.();
13594
- const response = await runtimePool.processDirect(request);
14361
+ const response = await dispatchPrompt(request);
13595
14362
  const replyText = typeof response === "string" ? response : String(response ?? "");
13596
14363
  if (replyText.trim()) await dispatcherOptions.deliver({ text: replyText }, { kind: "final" });
13597
14364
  } catch (error) {
@@ -13723,7 +14490,6 @@ function applyGatewayRuntimeCapabilityState(params) {
13723
14490
  params.state.pluginChannelBindings = params.next.pluginChannelBindings;
13724
14491
  }
13725
14492
  function configureGatewayPluginRuntime(params) {
13726
- params.gateway.reloader.setApplyAgentRuntimeConfig((nextConfig) => params.gateway.runtimePool.applyRuntimeConfig(nextConfig));
13727
14493
  params.gateway.reloader.setReloadPlugins(async ({ config: nextConfig, changedPaths }) => {
13728
14494
  const result = await reloadServicePlugins({
13729
14495
  nextConfig,
@@ -13746,9 +14512,7 @@ function configureGatewayPluginRuntime(params) {
13746
14512
  });
13747
14513
  params.state.pluginUiMetadata = getPluginUiMetadataFromRegistry(result.pluginRegistry);
13748
14514
  params.state.pluginGatewayHandles = result.pluginGatewayHandles;
13749
- params.gateway.runtimePool.applyExtensionRegistry(result.extensionRegistry);
13750
14515
  params.getLiveUiNcpAgent()?.applyExtensionRegistry?.(result.extensionRegistry);
13751
- params.gateway.runtimePool.applyRuntimeConfig(nextConfig);
13752
14516
  if (result.restartChannels) console.log("Config reload: plugin channel gateways restarted.");
13753
14517
  return { restartChannels: result.restartChannels };
13754
14518
  });
@@ -13756,7 +14520,12 @@ function configureGatewayPluginRuntime(params) {
13756
14520
  await params.getLiveUiNcpAgent()?.applyMcpConfig?.(nextConfig);
13757
14521
  });
13758
14522
  installPluginRuntimeBridge({
13759
- runtimePool: params.gateway.runtimePool,
14523
+ dispatchPrompt: async (request) => await dispatchPromptOverNcp({
14524
+ config: resolveConfigSecrets$2(loadConfig$2(), { configPath: params.gateway.runtimeConfigPath }),
14525
+ sessionManager: params.gateway.sessionManager,
14526
+ resolveNcpAgent: () => params.getLiveUiNcpAgent(),
14527
+ ...request
14528
+ }),
13760
14529
  runtimeConfigPath: params.gateway.runtimeConfigPath,
13761
14530
  getPluginChannelBindings: () => params.state.pluginChannelBindings
13762
14531
  });
@@ -13796,8 +14565,26 @@ function createDeferredGatewayStartupHooks(params) {
13796
14565
  };
13797
14566
  }
13798
14567
  //#endregion
14568
+ //#region src/cli/commands/service-support/gateway/service-gateway-runtime-lifecycle.ts
14569
+ function handleGatewayDeferredStartupError(params) {
14570
+ const message = params.error instanceof Error ? params.error.message : String(params.error);
14571
+ params.bootstrapStatus.markError(message);
14572
+ if (params.bootstrapStatus.getStatus().pluginHydration.state === "running") params.bootstrapStatus.markPluginHydrationError(message);
14573
+ console.error(`Deferred startup failed: ${params.error instanceof Error ? params.error.message : String(params.error)}`);
14574
+ }
14575
+ async function cleanupGatewayRuntime(params) {
14576
+ localUiRuntimeStore.clearIfOwnedByProcess();
14577
+ await params.fileWatchers.clear();
14578
+ params.resetRuntimeState();
14579
+ params.clearRealtimeBridge();
14580
+ await params.uiStartup?.deferredNcpAgent.close();
14581
+ await params.remoteModule?.stop();
14582
+ await stopPluginChannelGateways(params.runtimeState?.pluginGatewayHandles ?? []);
14583
+ setPluginRuntimeBridge(null);
14584
+ }
14585
+ //#endregion
13799
14586
  //#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;
14587
+ 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
14588
  function createSkillsLoader(workspace) {
13802
14589
  const ctor = NextclawCore.SkillsLoader;
13803
14590
  if (!ctor) return null;
@@ -13807,10 +14594,14 @@ var ServiceCommands = class {
13807
14594
  applyLiveConfigReload = null;
13808
14595
  liveUiNcpAgent = null;
13809
14596
  fileWatchers = new ServiceFileWatcherRegistry();
14597
+ loggingRuntime = NextclawCore.getLoggingRuntime();
14598
+ serviceLogger = this.loggingRuntime.getLogger("service");
14599
+ loggingInstalled = false;
13810
14600
  constructor(deps) {
13811
14601
  this.deps = deps;
13812
14602
  }
13813
14603
  startGateway = async (options = {}) => {
14604
+ this.ensureRuntimeLoggingInstalled();
13814
14605
  logStartupTrace("service.start_gateway.begin");
13815
14606
  await this.fileWatchers.clear();
13816
14607
  this.applyLiveConfigReload = null;
@@ -13871,9 +14662,11 @@ var ServiceCommands = class {
13871
14662
  initialPluginRegistry: createEmptyPluginRegistry(),
13872
14663
  makeProvider: (config, providerOptions) => providerOptions?.allowMissing === true ? this.makeProvider(config, { allowMissing: true }) : this.makeProvider(config),
13873
14664
  makeMissingProvider: (config) => this.makeMissingProvider(config),
13874
- requestRestart: (params) => this.deps.requestRestart(params)
14665
+ requestRestart: (params) => this.deps.requestRestart(params),
14666
+ getLiveUiNcpAgent: () => this.liveUiNcpAgent
13875
14667
  }));
13876
14668
  this.applyLiveConfigReload = gateway.applyLiveConfigReload;
14669
+ const loadGatewayConfig = () => resolveConfigSecrets$1(loadConfig$1(), { configPath: gateway.runtimeConfigPath });
13877
14670
  const gatewayRuntimeState = createGatewayRuntimeState(gateway);
13878
14671
  runtimeState = gatewayRuntimeState;
13879
14672
  uiStartup?.publish({
@@ -13889,10 +14682,6 @@ var ServiceCommands = class {
13889
14682
  state: gatewayRuntimeState,
13890
14683
  getLiveUiNcpAgent: () => this.liveUiNcpAgent
13891
14684
  });
13892
- wireSystemSessionUpdatedPublisher({
13893
- runtimePool: gateway.runtimePool,
13894
- publishUiEvent: uiStartup?.publish
13895
- });
13896
14685
  console.log("✓ Capability hydration: scheduled in background");
13897
14686
  await measureStartupAsync("service.start_gateway_support_services", async () => await startGatewayRuntimeSupport({
13898
14687
  cronJobs: gateway.cron.status().jobs,
@@ -13922,49 +14711,37 @@ var ServiceCommands = class {
13922
14711
  sessionManager: gateway.sessionManager
13923
14712
  })
13924
14713
  });
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
14714
+ await runConfiguredGatewayRuntime({
14715
+ uiStartup,
14716
+ gateway,
14717
+ deferredNcpSessionService: ncpSessionRealtimeBridge.deferredSessionService,
14718
+ getConfig: loadGatewayConfig,
14719
+ getExtensionRegistry: () => gatewayRuntimeState.extensionRegistry,
14720
+ resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
14721
+ registry: gatewayRuntimeState.pluginRegistry,
14722
+ channel,
14723
+ cfg: loadGatewayConfig(),
14724
+ accountId
13950
14725
  }),
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
- }
14726
+ deferredStartupHooks: deferredGatewayStartupHooks,
14727
+ getLiveUiNcpAgent: () => this.liveUiNcpAgent,
14728
+ publishSessionChange: ncpSessionRealtimeBridge.publishSessionChange,
14729
+ publishUiEvent: uiStartup?.publish,
14730
+ onDeferredStartupError: (error) => handleGatewayDeferredStartupError({
14731
+ bootstrapStatus,
14732
+ error
14733
+ }),
14734
+ cleanup: async () => await cleanupGatewayRuntime({
14735
+ fileWatchers: this.fileWatchers,
14736
+ resetRuntimeState: () => {
14737
+ this.applyLiveConfigReload = null;
14738
+ this.liveUiNcpAgent = null;
14739
+ },
14740
+ clearRealtimeBridge: () => ncpSessionRealtimeBridge.clear(),
14741
+ uiStartup,
14742
+ remoteModule: gateway.remoteModule,
14743
+ runtimeState
14744
+ })
13968
14745
  });
13969
14746
  logStartupTrace("service.start_gateway.end");
13970
14747
  };
@@ -14068,7 +14845,7 @@ var ServiceCommands = class {
14068
14845
  if (binding.host !== uiConfig.host || binding.port !== uiConfig.port) {
14069
14846
  console.log(`Detected running service UI bind (${binding.host}:${binding.port}); enforcing (${uiConfig.host}:${uiConfig.port})...`);
14070
14847
  await this.stopService();
14071
- const stateAfterStop = readServiceState();
14848
+ const stateAfterStop = managedServiceStateStore.read();
14072
14849
  if (stateAfterStop && isProcessRunning(stateAfterStop.pid)) {
14073
14850
  process.exitCode = 1;
14074
14851
  console.error("Error: Failed to stop running service while enforcing public UI exposure.");
@@ -14083,12 +14860,14 @@ var ServiceCommands = class {
14083
14860
  return true;
14084
14861
  };
14085
14862
  startService = async (options) => {
14863
+ this.loggingRuntime.ensureReady();
14864
+ const { open, startupTimeoutMs, uiOverrides } = options;
14086
14865
  const config = loadConfig$1();
14087
- const uiConfig = resolveUiConfig(config, options.uiOverrides);
14866
+ const uiConfig = resolveUiConfig(config, uiOverrides);
14088
14867
  const uiUrl = resolveUiApiBase(uiConfig.host, uiConfig.port);
14089
14868
  const apiUrl = `${uiUrl}/api`;
14090
14869
  const staticDir = resolveUiStaticDir();
14091
- const existing = readServiceState();
14870
+ const existing = managedServiceStateStore.read();
14092
14871
  if (existing && isProcessRunning(existing.pid)) {
14093
14872
  await this.handleExistingManagedService({
14094
14873
  existing,
@@ -14097,7 +14876,7 @@ var ServiceCommands = class {
14097
14876
  });
14098
14877
  return;
14099
14878
  }
14100
- if (existing) clearServiceState();
14879
+ if (existing) managedServiceStateStore.clear();
14101
14880
  if (!staticDir) {
14102
14881
  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
14882
  return;
@@ -14112,6 +14891,40 @@ var ServiceCommands = class {
14112
14891
  process.exitCode = 1, console.error(`Error: Cannot start ${APP_NAME$1} because UI port ${uiConfig.port} is already occupied.`), console.error(portPreflight.message);
14113
14892
  return;
14114
14893
  }
14894
+ if (portPreflight.reusedExistingHealthyTarget) {
14895
+ await this.reuseExistingHealthyStartTarget({
14896
+ uiConfig,
14897
+ uiUrl,
14898
+ apiUrl,
14899
+ open
14900
+ });
14901
+ return;
14902
+ }
14903
+ await this.startNewManagedServiceTarget({
14904
+ config,
14905
+ uiConfig,
14906
+ uiUrl,
14907
+ apiUrl,
14908
+ healthUrl,
14909
+ startupTimeoutMs
14910
+ });
14911
+ if (open) openBrowser(uiUrl);
14912
+ };
14913
+ reuseExistingHealthyStartTarget = async (params) => {
14914
+ const { apiUrl, open, uiConfig, uiUrl } = params;
14915
+ console.log(`✓ ${APP_NAME$1} is already serving the target UI/API port`);
14916
+ console.log(`UI: ${uiUrl}`);
14917
+ console.log(`API: ${apiUrl}`);
14918
+ console.warn([
14919
+ `Warning: The healthy listener on ${uiConfig.port} is not tracked by ${managedServiceStateStore.path}.`,
14920
+ "This start call reused the existing runtime instead of spawning another one.",
14921
+ "Use the owning process or port-level tools to stop it; managed stop/restart will not control it automatically."
14922
+ ].join(" "));
14923
+ await this.printPublicUiUrls(uiConfig.host, uiConfig.port);
14924
+ if (open) openBrowser(uiUrl);
14925
+ };
14926
+ startNewManagedServiceTarget = async (params) => {
14927
+ const { apiUrl, config, healthUrl, startupTimeoutMs, uiConfig, uiUrl } = params;
14115
14928
  const startup = spawnManagedService({
14116
14929
  appName: APP_NAME$1,
14117
14930
  config,
@@ -14119,13 +14932,14 @@ var ServiceCommands = class {
14119
14932
  uiUrl,
14120
14933
  apiUrl,
14121
14934
  healthUrl,
14122
- startupTimeoutMs: options.startupTimeoutMs,
14935
+ startupTimeoutMs,
14123
14936
  resolveStartupTimeoutMs: this.resolveStartupTimeoutMs,
14124
14937
  appendStartupStage: this.appendStartupStage,
14125
14938
  printStartupFailureDiagnostics: this.printStartupFailureDiagnostics,
14126
14939
  resolveServiceLogPath
14127
14940
  });
14128
14941
  if (!startup) {
14942
+ this.serviceLogger.fatal("managed service startup aborted", { reason: "child_process_not_created" });
14129
14943
  process.exitCode = 1;
14130
14944
  return;
14131
14945
  }
@@ -14141,22 +14955,27 @@ var ServiceCommands = class {
14141
14955
  waitForBackgroundServiceReady: this.waitForBackgroundServiceReady,
14142
14956
  isProcessRunning
14143
14957
  });
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
- }
14958
+ if (!readiness.ready && !isProcessRunning(startup.snapshot.pid)) {
14959
+ process.exitCode = 1;
14960
+ managedServiceStateStore.clear();
14961
+ const hint = readiness.lastProbeError ? ` Last probe error: ${readiness.lastProbeError}` : "";
14962
+ this.appendStartupStage(startup.logPath, `startup failed: process exited before ready.${hint}`);
14963
+ this.serviceLogger.fatal("managed service exited before readiness completed", {
14964
+ uiUrl,
14965
+ apiUrl,
14966
+ healthUrl,
14967
+ logPath: startup.logPath,
14968
+ ...readiness.lastProbeError ? { lastProbeError: readiness.lastProbeError } : {}
14969
+ });
14970
+ console.error(`Error: Failed to start background service. Check logs: ${startup.logPath}.${hint}`);
14971
+ this.printStartupFailureDiagnostics({
14972
+ uiUrl,
14973
+ apiUrl,
14974
+ healthUrl,
14975
+ logPath: startup.logPath,
14976
+ lastProbeError: readiness.lastProbeError
14977
+ });
14978
+ return;
14160
14979
  }
14161
14980
  startup.child.unref();
14162
14981
  await reportManagedServiceStart({
@@ -14174,17 +14993,16 @@ var ServiceCommands = class {
14174
14993
  printPublicUiUrls: this.printPublicUiUrls,
14175
14994
  printServiceControlHints: this.printServiceControlHints
14176
14995
  });
14177
- if (options.open) openBrowser(uiUrl);
14178
14996
  };
14179
14997
  stopService = async () => {
14180
- const state = readServiceState();
14998
+ const state = managedServiceStateStore.read();
14181
14999
  if (!state) {
14182
- console.log("No running service found.");
15000
+ console.log("No running background service found.");
14183
15001
  return;
14184
15002
  }
14185
15003
  if (!isProcessRunning(state.pid)) {
14186
15004
  console.log("Service is not running. Cleaning up state.");
14187
- clearServiceState();
15005
+ managedServiceStateStore.clear();
14188
15006
  return;
14189
15007
  }
14190
15008
  console.log(`Stopping ${APP_NAME$1} (PID ${state.pid})...`);
@@ -14203,7 +15021,8 @@ var ServiceCommands = class {
14203
15021
  }
14204
15022
  await waitForExit(state.pid, 2e3);
14205
15023
  }
14206
- clearServiceState();
15024
+ managedServiceStateStore.clear();
15025
+ localUiRuntimeStore.clearIfOwnedByProcess(state.pid);
14207
15026
  console.log(`✓ ${APP_NAME$1} stopped`);
14208
15027
  };
14209
15028
  waitForBackgroundServiceReady = async (params) => {
@@ -14243,14 +15062,14 @@ var ServiceCommands = class {
14243
15062
  };
14244
15063
  appendStartupStage = (logPath, message) => {
14245
15064
  try {
14246
- appendFileSync(logPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [startup] ${message}\n`, "utf-8");
15065
+ this.serviceLogger.child("startup").info(message, { logPath });
14247
15066
  } catch (error) {
14248
15067
  const detail = error instanceof Error ? error.message : String(error);
14249
15068
  console.error(`Warning: failed to write startup diagnostics log (${logPath}): ${detail}`);
14250
15069
  }
14251
15070
  };
14252
15071
  printStartupFailureDiagnostics = (params) => {
14253
- const statePath = resolveServiceStatePath();
15072
+ const statePath = managedServiceStateStore.path;
14254
15073
  const lines = [
14255
15074
  "Startup diagnostics:",
14256
15075
  `- UI URL: ${params.uiUrl}`,
@@ -14264,20 +15083,22 @@ var ServiceCommands = class {
14264
15083
  };
14265
15084
  checkUiPortPreflight = async (params) => {
14266
15085
  const { healthUrl, host, port } = params;
14267
- const availability = await checkPortAvailability({
15086
+ const target = await inspectUiTarget({
14268
15087
  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
- }
15088
+ port,
15089
+ healthUrl
15090
+ });
15091
+ if (target.state === "available") return {
15092
+ ok: true,
15093
+ reusedExistingHealthyTarget: false
15094
+ };
15095
+ if (target.state === "healthy-existing") return {
15096
+ ok: true,
15097
+ reusedExistingHealthyTarget: true
15098
+ };
15099
+ const lines = [`Port probe: ${target.availabilityDetail}`];
15100
+ if (target.probeError) lines.push(`Health probe: ${target.probeError}`);
15101
+ lines.push("The port is occupied by a process that does not answer as a healthy NextClaw HTTP server.");
14281
15102
  lines.push(`Fix: free port ${port} or start NextClaw with another port via --ui-port <port>.`);
14282
15103
  lines.push(`Inspect locally with: ss -ltnp | grep ${port} || lsof -iTCP:${port} -sTCP:LISTEN -n -P`);
14283
15104
  return {
@@ -14334,6 +15155,17 @@ var ServiceCommands = class {
14334
15155
  console.log("Service controls:");
14335
15156
  console.log(` - Check status: ${APP_NAME$1} status`);
14336
15157
  console.log(` - If you need to stop the service, run: ${APP_NAME$1} stop`);
15158
+ console.log(` - View log paths: ${APP_NAME$1} logs path`);
15159
+ console.log(` - Tail recent logs: ${APP_NAME$1} logs tail`);
15160
+ };
15161
+ ensureRuntimeLoggingInstalled = () => {
15162
+ if (this.loggingInstalled) return;
15163
+ NextclawCore.configureAppLogging({
15164
+ installConsoleMirror: true,
15165
+ installProcessCrashMonitor: true
15166
+ });
15167
+ this.serviceLogger.info("runtime logging ready", { startupId: this.loggingRuntime.getStartupId() });
15168
+ this.loggingInstalled = true;
14337
15169
  };
14338
15170
  installBuiltinMarketplaceSkill = (slug, _force) => {
14339
15171
  if (!(createSkillsLoader(getWorkspacePath$1(loadConfig$1().agents.defaults.workspace))?.listSkills(false) ?? []).find((skill) => skill.name === slug && skill.source === "builtin")) return null;
@@ -14538,8 +15370,7 @@ var WorkspaceManager = class {
14538
15370
  }
14539
15371
  };
14540
15372
  //#endregion
14541
- //#region src/cli/runtime.ts
14542
- const LOGO = "🤖";
15373
+ //#region src/cli/commands/agent/cli-agent-runner.ts
14543
15374
  const EXIT_COMMANDS = new Set([
14544
15375
  "exit",
14545
15376
  "quit",
@@ -14547,6 +15378,91 @@ const EXIT_COMMANDS = new Set([
14547
15378
  "/quit",
14548
15379
  ":q"
14549
15380
  ]);
15381
+ function buildCliSharedMetadata(opts) {
15382
+ return typeof opts.model === "string" && opts.model.trim() ? { model: opts.model.trim() } : {};
15383
+ }
15384
+ function createCliHistoryInterface() {
15385
+ const historyFile = join(getDataDir(), "history", "cli_history");
15386
+ mkdirSync(resolve(historyFile, ".."), { recursive: true });
15387
+ const history = existsSync(historyFile) ? readFileSync(historyFile, "utf-8").split("\n").filter(Boolean) : [];
15388
+ const rl = createInterface({
15389
+ input: process.stdin,
15390
+ output: process.stdout
15391
+ });
15392
+ rl.on("close", () => {
15393
+ writeFileSync(historyFile, history.concat(rl.history ?? []).join("\n"));
15394
+ process.exit(0);
15395
+ });
15396
+ return rl;
15397
+ }
15398
+ async function runCliInteractiveLoop(params) {
15399
+ console.log(`${params.logo} Interactive mode (type exit or Ctrl+C to quit)\n`);
15400
+ const rl = createCliHistoryInterface();
15401
+ let running = true;
15402
+ while (running) {
15403
+ const trimmed = (await prompt(rl, "You: ")).trim();
15404
+ if (!trimmed) continue;
15405
+ if (EXIT_COMMANDS.has(trimmed.toLowerCase())) {
15406
+ rl.close();
15407
+ running = false;
15408
+ break;
15409
+ }
15410
+ printAgentResponse(await dispatchPromptOverNcp({
15411
+ config: params.config,
15412
+ sessionManager: params.sessionManager,
15413
+ resolveNcpAgent: () => params.ncpAgent,
15414
+ sessionKey: params.sessionKey,
15415
+ content: trimmed,
15416
+ metadata: params.metadata
15417
+ }));
15418
+ }
15419
+ }
15420
+ async function runCliAgentCommand(params) {
15421
+ const bus = new MessageBus();
15422
+ const sessionManager = new SessionManager({
15423
+ workspace: params.workspace,
15424
+ homeDir: getDataDir()
15425
+ });
15426
+ const ncpAgent = await createUiNcpAgent({
15427
+ bus,
15428
+ providerManager: params.providerManager,
15429
+ sessionManager,
15430
+ getConfig: params.loadResolvedConfig,
15431
+ getExtensionRegistry: () => params.extensionRegistry,
15432
+ resolveMessageToolHints: ({ channel, accountId }) => params.resolveMessageToolHints({
15433
+ channel,
15434
+ accountId
15435
+ })
15436
+ });
15437
+ try {
15438
+ const sessionKey = params.opts.session ?? "cli:default";
15439
+ const sharedMetadata = buildCliSharedMetadata(params.opts);
15440
+ if (params.opts.message) {
15441
+ printAgentResponse(await dispatchPromptOverNcp({
15442
+ config: params.config,
15443
+ sessionManager,
15444
+ resolveNcpAgent: () => ncpAgent,
15445
+ sessionKey,
15446
+ content: params.opts.message,
15447
+ metadata: sharedMetadata
15448
+ }));
15449
+ return;
15450
+ }
15451
+ await runCliInteractiveLoop({
15452
+ logo: params.logo,
15453
+ config: params.config,
15454
+ sessionManager,
15455
+ ncpAgent,
15456
+ sessionKey,
15457
+ metadata: sharedMetadata
15458
+ });
15459
+ } finally {
15460
+ await ncpAgent.dispose?.();
15461
+ }
15462
+ }
15463
+ //#endregion
15464
+ //#region src/cli/runtime.ts
15465
+ const LOGO = "🤖";
14550
15466
  const FORCED_PUBLIC_UI_HOST = "0.0.0.0";
14551
15467
  var CliRuntime = class {
14552
15468
  logo;
@@ -14566,6 +15482,7 @@ var CliRuntime = class {
14566
15482
  remoteCommands;
14567
15483
  remote;
14568
15484
  diagnosticsCommands;
15485
+ logsCommands;
14569
15486
  constructor(options = {}) {
14570
15487
  logStartupTrace("cli.runtime.constructor.begin");
14571
15488
  this.logo = options.logo ?? "🤖";
@@ -14598,8 +15515,9 @@ var CliRuntime = class {
14598
15515
  hasRunningManagedService: hasRunningNextclawManagedService
14599
15516
  }));
14600
15517
  this.diagnosticsCommands = measureStartupSync("cli.runtime.diagnostics_commands", () => new DiagnosticsCommands({ logo: this.logo }));
15518
+ this.logsCommands = measureStartupSync("cli.runtime.logs_commands", () => new LogsCommands());
14601
15519
  this.restartCoordinator = measureStartupSync("cli.runtime.restart_coordinator", () => new RestartCoordinator({
14602
- readServiceState,
15520
+ readServiceState: managedServiceStateStore.read,
14603
15521
  isProcessRunning,
14604
15522
  currentPid: () => process.pid,
14605
15523
  restartBackgroundService: async (reason) => this.restartBackgroundService(reason),
@@ -14619,7 +15537,7 @@ var CliRuntime = class {
14619
15537
  restartBackgroundService = async (reason) => {
14620
15538
  if (this.serviceRestartTask) return this.serviceRestartTask;
14621
15539
  this.serviceRestartTask = (async () => {
14622
- const state = readServiceState();
15540
+ const state = managedServiceStateStore.read();
14623
15541
  if (!state || !isProcessRunning(state.pid) || state.pid === process.pid) return false;
14624
15542
  const uiHost = FORCED_PUBLIC_UI_HOST;
14625
15543
  const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 55667;
@@ -14645,7 +15563,7 @@ var CliRuntime = class {
14645
15563
  const strategy = params.strategy ?? "background-service-or-manual";
14646
15564
  if (strategy !== "background-service-or-exit" && strategy !== "exit-process") return;
14647
15565
  if (this.selfRelaunchArmed) return;
14648
- const state = readServiceState();
15566
+ const state = managedServiceStateStore.read();
14649
15567
  if (!state || state.pid !== process.pid) return;
14650
15568
  const uiPort = typeof state.uiPort === "number" && Number.isFinite(state.uiPort) ? state.uiPort : 55667;
14651
15569
  const delayMs = typeof params.delayMs === "number" && Number.isFinite(params.delayMs) ? Math.max(0, Math.floor(params.delayMs)) : 100;
@@ -14655,7 +15573,7 @@ var CliRuntime = class {
14655
15573
  "--ui-port",
14656
15574
  String(uiPort)
14657
15575
  ];
14658
- const serviceStatePath = resolve(getDataDir(), "run", "service.json");
15576
+ const serviceStatePath = managedServiceStateStore.path;
14659
15577
  const helperScript = [
14660
15578
  "const { spawnSync } = require(\"node:child_process\");",
14661
15579
  "const { readFileSync } = require(\"node:fs\");",
@@ -14840,13 +15758,13 @@ var CliRuntime = class {
14840
15758
  uiPort: opts.uiPort,
14841
15759
  forcedPublicHost: FORCED_PUBLIC_UI_HOST
14842
15760
  });
14843
- const state = readServiceState();
15761
+ const state = managedServiceStateStore.read();
14844
15762
  if (state && isProcessRunning(state.pid)) {
14845
15763
  console.log(`Restarting ${APP_NAME}...`);
14846
15764
  await this.serviceCommands.stopService();
14847
15765
  } else {
14848
15766
  if (state) {
14849
- clearServiceState();
15767
+ managedServiceStateStore.clear();
14850
15768
  console.log("Service state was stale and has been cleaned up.");
14851
15769
  }
14852
15770
  const unmanagedHealthyServiceMessage = await describeUnmanagedHealthyTargetMessage({ uiOverrides });
@@ -14888,22 +15806,19 @@ var CliRuntime = class {
14888
15806
  }
14889
15807
  });
14890
15808
  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,
15809
+ const provider = this.serviceCommands.createProvider(config) ?? this.serviceCommands.createMissingProvider(config);
15810
+ const providerManager = this.createObservedProviderManager(new ProviderManager({
15811
+ defaultProvider: provider,
15812
+ config
15813
+ }), "cli-agent");
15814
+ await runCliAgentCommand({
15815
+ logo: this.logo,
15816
+ opts,
14905
15817
  config,
15818
+ workspace,
15819
+ providerManager,
14906
15820
  extensionRegistry,
15821
+ loadResolvedConfig: () => resolveConfigSecrets(loadConfig(), { configPath }),
14907
15822
  resolveMessageToolHints: ({ channel, accountId }) => resolvePluginChannelMessageToolHints({
14908
15823
  registry: pluginRegistry,
14909
15824
  channel,
@@ -14911,43 +15826,6 @@ var CliRuntime = class {
14911
15826
  accountId
14912
15827
  })
14913
15828
  });
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
15829
  } finally {
14952
15830
  setPluginRuntimeBridge(null);
14953
15831
  }
@@ -14984,7 +15862,7 @@ var CliRuntime = class {
14984
15862
  console.log(`✓ Update complete (${result.strategy})`);
14985
15863
  if (versionAfter === versionBefore) console.log(`Version unchanged: ${versionBefore}`);
14986
15864
  else console.log(`Version updated: ${versionBefore} -> ${versionAfter}`);
14987
- const state = readServiceState();
15865
+ const state = managedServiceStateStore.read();
14988
15866
  if (state && isProcessRunning(state.pid)) console.log(`Tip: restart ${APP_NAME} to apply the update.`);
14989
15867
  };
14990
15868
  agentsList = (opts = {}) => {
@@ -15092,6 +15970,12 @@ var CliRuntime = class {
15092
15970
  doctor = async (opts = {}) => {
15093
15971
  await this.diagnosticsCommands.doctor(opts);
15094
15972
  };
15973
+ logsPath = () => {
15974
+ this.logsCommands.logsPath();
15975
+ };
15976
+ logsTail = (opts = {}) => {
15977
+ this.logsCommands.logsTail(opts);
15978
+ };
15095
15979
  skillsInstall = async (options) => {
15096
15980
  const config = loadConfig();
15097
15981
  const workdir = resolveSkillsInstallWorkdir({
@@ -15119,6 +16003,7 @@ var CliRuntime = class {
15119
16003
  console.log(` Alias: ${result.slug}`);
15120
16004
  console.log(` Files: ${result.fileCount}`);
15121
16005
  };
16006
+ createObservedProviderManager = (providerManager, source) => new ObservedProviderManager(providerManager, new LlmUsageObserver(llmUsageRecorder, source));
15122
16007
  };
15123
16008
  //#endregion
15124
16009
  //#region src/cli/register-agents-commands.ts
@@ -15135,6 +16020,7 @@ function registerAgentsCommands(program, runtime) {
15135
16020
  logStartupTrace("cli.index.module_loaded");
15136
16021
  const program = new Command();
15137
16022
  const runtime = measureStartupSync("cli.runtime.construct", () => new CliRuntime({ logo: LOGO }));
16023
+ const llmUsageCommands = new LlmUsageCommands();
15138
16024
  program.name(APP_NAME).description(`${LOGO} ${APP_NAME} - ${APP_TAGLINE}`).version(getPackageVersion$1(), "-v, --version", "show version");
15139
16025
  program.command("onboard").description(`Initialize ${APP_NAME} configuration and workspace`).action(async () => runtime.onboard());
15140
16026
  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 +16089,10 @@ cron.command("disable <jobId>").action(async (jobId) => runtime.cronEnable(jobId
15203
16089
  cron.command("run <jobId>").option("-f, --force", "Run even if disabled").action(async (jobId, opts) => runtime.cronRun(jobId, opts));
15204
16090
  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
16091
  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));
16092
+ const logs = program.command("logs").description("Inspect local runtime logs");
16093
+ logs.command("path").description("Show local log file paths").action(() => runtime.logsPath());
16094
+ 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));
16095
+ 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
16096
  program.parseAsync(process.argv);
15207
16097
  //#endregion
15208
16098
  export {};