@intent-systems/nexus 2026.1.5-3 → 2026.1.5-5

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 (144) hide show
  1. package/dist/agents/agent-id.js +41 -0
  2. package/dist/agents/auth-profiles.js +114 -25
  3. package/dist/agents/identity-state.js +79 -0
  4. package/dist/agents/model-auth.js +1 -0
  5. package/dist/agents/model-fallback.js +15 -9
  6. package/dist/agents/model-selection.js +1 -1
  7. package/dist/agents/models-config.js +17 -11
  8. package/dist/agents/pi-embedded-runner.js +101 -9
  9. package/dist/agents/sandbox.js +12 -3
  10. package/dist/agents/skill-runner.js +29 -4
  11. package/dist/agents/skill-usage.js +114 -11
  12. package/dist/agents/skills-status.js +4 -4
  13. package/dist/agents/skills.js +18 -7
  14. package/dist/agents/subagent-registry.js +25 -11
  15. package/dist/agents/system-prompt.js +16 -0
  16. package/dist/agents/tool-policy.js +19 -3
  17. package/dist/agents/tools/browser-tool.js +5 -2
  18. package/dist/agents/tools/image-tool.js +93 -8
  19. package/dist/agents/tools/sessions-announce-target.js +5 -1
  20. package/dist/agents/workspace.js +55 -46
  21. package/dist/auto-reply/command-detection.js +2 -1
  22. package/dist/auto-reply/reply/directive-handling.js +153 -28
  23. package/dist/auto-reply/reply/directives.js +17 -2
  24. package/dist/auto-reply/reply/model-selection.js +8 -3
  25. package/dist/auto-reply/reply/queue.js +2 -2
  26. package/dist/auto-reply/reply.js +1 -1
  27. package/dist/auto-reply/thinking.js +15 -0
  28. package/dist/browser/chrome.js +1 -1
  29. package/dist/browser/client.js +2 -0
  30. package/dist/browser/config.js +6 -2
  31. package/dist/browser/pw-tools-core.js +3 -0
  32. package/dist/browser/routes/agent.js +14 -0
  33. package/dist/canvas-host/server.js +1 -1
  34. package/dist/capabilities/detector.js +245 -0
  35. package/dist/capabilities/registry.js +99 -0
  36. package/dist/channels/location.js +44 -0
  37. package/dist/channels/web/index.js +2 -0
  38. package/dist/cli/cloud-cli.js +12 -7
  39. package/dist/cli/credential-cli.js +139 -17
  40. package/dist/cli/gateway-cli.js +1 -1
  41. package/dist/cli/log-cli.js +25 -0
  42. package/dist/cli/pairing-cli.js +1 -1
  43. package/dist/cli/program.js +58 -6
  44. package/dist/cli/run-main.js +1 -1
  45. package/dist/cli/skills-cli.js +144 -21
  46. package/dist/cli/skills-hub-cli.js +59 -29
  47. package/dist/cli/tool-connector-cli.js +99 -24
  48. package/dist/cli/upstream-sync-cli.js +253 -96
  49. package/dist/cli/usage-cli.js +14 -0
  50. package/dist/commands/auth-choice-options.js +6 -1
  51. package/dist/commands/auth-choice.js +157 -5
  52. package/dist/commands/bootstrap-preset.js +10 -6
  53. package/dist/commands/capabilities.js +33 -6
  54. package/dist/commands/claude-md.js +3 -2
  55. package/dist/commands/config-view.js +1 -1
  56. package/dist/commands/configure.js +4 -4
  57. package/dist/commands/credential.js +497 -36
  58. package/dist/commands/cursor-rules.js +39 -19
  59. package/dist/commands/doctor.js +5 -4
  60. package/dist/commands/identity.js +28 -31
  61. package/dist/commands/init.js +15 -18
  62. package/dist/commands/log.js +134 -0
  63. package/dist/commands/models/fallbacks.js +1 -1
  64. package/dist/commands/models/image-fallbacks.js +1 -1
  65. package/dist/commands/models/list.js +1 -1
  66. package/dist/commands/models/scan.js +1 -1
  67. package/dist/commands/onboard-auth.js +27 -2
  68. package/dist/commands/onboard-eve-identity.js +7 -8
  69. package/dist/commands/onboard-non-interactive.js +4 -2
  70. package/dist/commands/onboard-quickstart.js +18 -11
  71. package/dist/commands/quest-state.js +271 -0
  72. package/dist/commands/quest.js +53 -13
  73. package/dist/commands/reset.js +1 -1
  74. package/dist/commands/sessions-ingest.js +5 -4
  75. package/dist/commands/setup.js +4 -2
  76. package/dist/commands/skills-manifest.js +2 -2
  77. package/dist/commands/status.js +179 -61
  78. package/dist/commands/suggestions.js +1 -1
  79. package/dist/commands/usage-tracking.js +32 -0
  80. package/dist/commands/usage-upload.js +6 -1
  81. package/dist/config/defaults.js +1 -3
  82. package/dist/config/includes.js +5 -7
  83. package/dist/config/io.js +88 -16
  84. package/dist/config/legacy.js +4 -2
  85. package/dist/config/paths.js +16 -0
  86. package/dist/config/sessions.js +9 -5
  87. package/dist/config/zod-schema.js +4 -3
  88. package/dist/control-plane/broker/broker.js +1022 -0
  89. package/dist/control-plane/compaction.js +282 -0
  90. package/dist/control-plane/factory.js +31 -0
  91. package/dist/control-plane/index.js +10 -0
  92. package/dist/control-plane/odu/agents.js +192 -0
  93. package/dist/control-plane/odu/interaction-tools.js +208 -0
  94. package/dist/control-plane/odu/prompt-loader.js +95 -0
  95. package/dist/control-plane/odu/runtime.js +479 -0
  96. package/dist/control-plane/odu/types.js +6 -0
  97. package/dist/control-plane/odu-control-plane.js +316 -0
  98. package/dist/control-plane/single-agent.js +249 -0
  99. package/dist/control-plane/types.js +11 -0
  100. package/dist/credentials/store.js +449 -0
  101. package/dist/gateway/server-browser.js +5 -4
  102. package/dist/gateway/server-methods/cron.js +11 -1
  103. package/dist/gateway/server.js +14 -7
  104. package/dist/infra/bonjour.js +1 -1
  105. package/dist/infra/event-log.js +8 -2
  106. package/dist/infra/path-env.js +1 -2
  107. package/dist/infra/provider-usage.auth.js +5 -3
  108. package/dist/infra/provider-usage.fetch.claude.js +16 -6
  109. package/dist/infra/provider-usage.fetch.minimax.js +8 -3
  110. package/dist/infra/provider-usage.js +9 -5
  111. package/dist/infra/restart.js +2 -2
  112. package/dist/infra/usage-settings.js +78 -0
  113. package/dist/infra/usage-suggestions.js +17 -5
  114. package/dist/infra/usage-upload.js +38 -1
  115. package/dist/infra/voicewake.js +2 -2
  116. package/dist/logging/redact.js +109 -0
  117. package/dist/markdown/fences.js +58 -0
  118. package/dist/media/image-ops.js +3 -1
  119. package/dist/memory/embeddings.js +146 -0
  120. package/dist/memory/index.js +3 -0
  121. package/dist/memory/internal.js +163 -0
  122. package/dist/pairing/pairing-store.js +218 -0
  123. package/dist/plugins/cli.js +42 -0
  124. package/dist/plugins/discovery.js +253 -0
  125. package/dist/plugins/install.js +181 -0
  126. package/dist/plugins/loader.js +290 -0
  127. package/dist/plugins/registry.js +105 -0
  128. package/dist/plugins/status.js +29 -0
  129. package/dist/plugins/tools.js +39 -0
  130. package/dist/plugins/types.js +1 -0
  131. package/dist/providers/github-copilot-auth.js +1 -1
  132. package/dist/routing/resolve-route.js +144 -0
  133. package/dist/routing/session-key.js +65 -0
  134. package/dist/sessions/send-policy.js +5 -5
  135. package/dist/slack/monitor.js +22 -1
  136. package/dist/telegram/reaction-level.js +2 -1
  137. package/dist/utils/provider-utils.js +28 -0
  138. package/dist/utils.js +4 -3
  139. package/dist/wizard/onboarding.js +29 -7
  140. package/package.json +4 -29
  141. package/patches/@mariozechner__pi-ai.patch +215 -0
  142. package/patches/playwright-core@1.57.0.patch +13 -0
  143. package/patches/qrcode-terminal.patch +12 -0
  144. package/scripts/postinstall.js +202 -0
@@ -1,109 +1,184 @@
1
1
  import fs from "node:fs";
2
- import path from "node:path";
2
+ import { resolveIdentitySnapshot } from "../agents/identity-state.js";
3
+ import { getAggregateStats } from "../agents/skill-usage.js";
3
4
  import { detectCapabilities } from "../capabilities/detector.js";
4
- import { listCredentials } from "./credential.js";
5
- import { resolveStateDir } from "../config/paths.js";
6
5
  import { defaultRuntime } from "../runtime.js";
7
- function readField(pathname, label) {
8
- try {
9
- const raw = fs.readFileSync(pathname, "utf-8");
10
- const regex = new RegExp(`^[-*]?\\s*${label}\\s*:\\s*(.+)$`, "im");
11
- const match = raw.match(regex);
12
- return match?.[1]?.trim();
13
- }
14
- catch {
15
- return undefined;
16
- }
6
+ import { listCredentials } from "./credential.js";
7
+ import { resolveQuestState } from "./quest-state.js";
8
+ const STATUS_BOX_WIDTH = 62;
9
+ const POWER_BOX_WIDTH = 62;
10
+ function clampText(text, width) {
11
+ if (text.length <= width)
12
+ return text;
13
+ return text.slice(0, width);
17
14
  }
18
- function getIdentitySnapshot() {
19
- const stateDir = resolveStateDir();
20
- const agentId = process.env.NEXUS_AGENT_ID?.trim() || "default";
21
- const agentIdentityDir = path.join(stateDir, "agents", agentId, "identity");
22
- const userIdentityDir = path.join(stateDir, "user", "identity");
23
- const onboardingDir = path.join(stateDir, "nexus", "onboarding");
24
- const agentIdentityPath = path.join(agentIdentityDir, "IDENTITY.md");
25
- const agentSoulPath = path.join(agentIdentityDir, "SOUL.md");
26
- const agentMemoryPath = path.join(agentIdentityDir, "MEMORY.md");
27
- const userProfilePath = path.join(userIdentityDir, "PROFILE.md");
28
- const bootstrapPath = path.join(onboardingDir, "BOOTSTRAP.md");
29
- const hasIdentity = fs.existsSync(agentIdentityPath) && fs.existsSync(userProfilePath);
30
- const agentName = readField(agentIdentityPath, "Name");
31
- const userName = readField(userProfilePath, "Name");
32
- return {
33
- agentId,
34
- agentName,
35
- userName,
36
- agentIdentityPath,
37
- agentSoulPath,
38
- agentMemoryPath,
39
- userProfilePath,
40
- bootstrapPath,
41
- hasIdentity,
42
- };
15
+ function centerText(text, width) {
16
+ const clamped = clampText(text, width);
17
+ const pad = Math.max(0, width - clamped.length);
18
+ const left = Math.floor(pad / 2);
19
+ const right = pad - left;
20
+ return `${" ".repeat(left)}${clamped}${" ".repeat(right)}`;
21
+ }
22
+ function renderBox(lines, width) {
23
+ const top = `╔${"═".repeat(width)}╗`;
24
+ const bottom = `╚${"═".repeat(width)}╝`;
25
+ const body = lines.map((line) => `║${centerText(line, width)}║`);
26
+ return [top, ...body, bottom];
43
27
  }
28
+ const FALLBACK_BOOTSTRAP_PROMPT = `# BOOTSTRAP.md - Welcome to Nexus
29
+
30
+ Identity setup is required.
31
+
32
+ For humans: start a short conversation and share your name, goals, and preferences.
33
+ For agents: write PROFILE.md, IDENTITY.md, SOUL.md, and MEMORY.md as you learn them.
34
+ `;
44
35
  function readBootstrapPrompt(pathname) {
45
36
  try {
46
37
  return fs.readFileSync(pathname, "utf-8");
47
38
  }
48
39
  catch {
49
- return null;
40
+ return FALLBACK_BOOTSTRAP_PROMPT;
50
41
  }
51
42
  }
52
43
  function formatSuggestions(capabilities) {
53
44
  const picks = capabilities.filter((cap) => ["ready", "needs_setup", "needs_install", "broken"].includes(cap.status));
54
- const sorted = picks.sort((a, b) => a.status.localeCompare(b.status));
55
- return sorted.slice(0, 3).map((cap, idx) => ({
56
- id: cap.id,
57
- status: cap.status,
58
- index: idx + 1,
59
- }));
45
+ const priority = ["broken", "ready", "needs_setup", "needs_install"];
46
+ const sorted = picks.sort((a, b) => priority.indexOf(a.status) - priority.indexOf(b.status));
47
+ return sorted.slice(0, 3).map((cap, idx) => {
48
+ const action = cap.status === "broken"
49
+ ? "nexus credential list"
50
+ : `nexus capabilities --status ${cap.status.replace("_", "-")}`;
51
+ const title = cap.status === "ready"
52
+ ? `Try ${cap.id} (ready, never used)`
53
+ : cap.status === "needs_setup"
54
+ ? `Set up ${cap.id}`
55
+ : cap.status === "needs_install"
56
+ ? `Install ${cap.id}`
57
+ : `Fix ${cap.id}`;
58
+ return {
59
+ id: cap.id,
60
+ status: cap.status,
61
+ index: idx + 1,
62
+ title,
63
+ action,
64
+ };
65
+ });
60
66
  }
61
67
  export async function statusCommand(opts, runtime = defaultRuntime) {
62
- const identity = getIdentitySnapshot();
68
+ const identityResolution = resolveIdentitySnapshot();
69
+ if (!identityResolution.ok) {
70
+ const payload = {
71
+ ok: false,
72
+ error: "multiple_agents",
73
+ agents: identityResolution.agentOptions,
74
+ };
75
+ if (opts.json) {
76
+ runtime.log(JSON.stringify(payload, null, 2));
77
+ }
78
+ else {
79
+ runtime.error("Multiple agents detected in state/agents.");
80
+ runtime.error(`Set NEXUS_AGENT_ID to one of: ${identityResolution.agentOptions.join(", ")}`);
81
+ }
82
+ runtime.exit(2);
83
+ return;
84
+ }
85
+ const identity = identityResolution.snapshot;
63
86
  const bootstrapPrompt = !identity.hasIdentity
64
87
  ? readBootstrapPrompt(identity.bootstrapPath)
65
88
  : null;
66
89
  const showBrief = opts.brief === true;
67
90
  const showCredentials = opts.credentials === true || (!opts.capabilities && !showBrief);
68
91
  const showCapabilities = opts.capabilities === true || (!opts.credentials && !showBrief);
69
- const credentials = showCredentials ? await listCredentials() : null;
70
- const capabilitySnapshot = showCapabilities ? detectCapabilities() : null;
71
- const suggestions = capabilitySnapshot ? formatSuggestions(capabilitySnapshot.capabilities) : [];
92
+ const showUsage = opts.usage === true ||
93
+ (!showBrief && !opts.capabilities && !opts.credentials);
94
+ const shouldLoadCredentials = showCredentials || !showBrief;
95
+ const shouldLoadCapabilities = showCapabilities || !showBrief;
96
+ const credentials = shouldLoadCredentials ? await listCredentials() : null;
97
+ const capabilitySnapshot = shouldLoadCapabilities
98
+ ? detectCapabilities()
99
+ : null;
100
+ const suggestions = capabilitySnapshot
101
+ ? formatSuggestions(capabilitySnapshot.capabilities)
102
+ : [];
103
+ const usage = showUsage
104
+ ? await getAggregateStats({ windowDays: 7, limit: 3 })
105
+ : null;
106
+ const questState = showBrief
107
+ ? null
108
+ : await resolveQuestState({
109
+ identityConfigured: identity.hasIdentity,
110
+ capabilities: capabilitySnapshot?.capabilities ?? [],
111
+ credentials: credentials ?? {
112
+ version: 1,
113
+ lastUpdated: "",
114
+ services: {},
115
+ },
116
+ includeSecrets: true,
117
+ });
118
+ if (opts.quiet) {
119
+ runtime.exit(bootstrapPrompt ? 2 : 0);
120
+ return;
121
+ }
72
122
  if (opts.json) {
73
123
  runtime.log(JSON.stringify({
74
124
  platform: `${process.platform}/${process.arch}`,
75
125
  identity,
76
- bootstrap: bootstrapPrompt ? { required: true, prompt: bootstrapPrompt } : undefined,
126
+ bootstrap: bootstrapPrompt
127
+ ? { required: true, prompt: bootstrapPrompt }
128
+ : undefined,
77
129
  credentials: credentials ?? undefined,
78
130
  capabilities: capabilitySnapshot ?? undefined,
79
131
  suggestions,
132
+ usage: usage ?? undefined,
133
+ power: questState?.power ?? undefined,
80
134
  }, null, 2));
81
135
  return;
82
136
  }
83
- runtime.log(`\nNexus Status (${process.platform}/${process.arch})`);
137
+ for (const line of renderBox(["Nexus Status", `${process.platform}/${process.arch}`], STATUS_BOX_WIDTH)) {
138
+ runtime.log(line);
139
+ }
84
140
  if (bootstrapPrompt) {
85
- runtime.log("\nBootstrap required (identity missing).");
141
+ runtime.log("");
142
+ for (const line of renderBox(["Welcome to Nexus!", "Identity setup required."], STATUS_BOX_WIDTH)) {
143
+ runtime.log(line);
144
+ }
86
145
  runtime.log(`\nBootstrap prompt (${identity.bootstrapPath}):\n`);
87
146
  runtime.log(bootstrapPrompt.trimEnd());
88
147
  return;
89
148
  }
90
- runtime.log("\nIdentity");
149
+ runtime.log("\n👤 Identity");
91
150
  runtime.log(` User: ${identity.userName ?? "(unknown)"} → ${identity.userProfilePath}`);
92
151
  runtime.log(` Agent: ${identity.agentName ?? "(unknown)"} (${identity.agentId}) → ${identity.agentIdentityPath}`);
93
152
  runtime.log(` → ${identity.agentSoulPath}`);
94
153
  runtime.log(` → ${identity.agentMemoryPath}`);
95
154
  if (showCredentials && credentials) {
96
155
  const services = Object.entries(credentials.services ?? {});
97
- runtime.log(`\nCredentials (${services.length} services)`);
156
+ const accountCount = services.reduce((sum, [, info]) => sum + (info.accounts?.length ?? 0), 0);
157
+ runtime.log(`\n🔑 Credentials (${accountCount} configured)`);
158
+ const statusIcon = {
159
+ active: "✅",
160
+ ready: "⭐",
161
+ broken: "❌",
162
+ };
98
163
  for (const [service, info] of services) {
99
164
  const accounts = info.accounts ?? [];
100
- const summary = accounts.length > 0 ? `${accounts.length} account(s)` : "no accounts";
101
- runtime.log(` ${service}: ${summary}`);
165
+ if (accounts.length === 0) {
166
+ runtime.log(` ${service}: none`);
167
+ continue;
168
+ }
169
+ runtime.log(` ${service}`);
170
+ for (const account of accounts) {
171
+ const status = account.status ?? "ready";
172
+ const icon = statusIcon[status] ?? "•";
173
+ const suffix = account.lastError ? ` - ${account.lastError}` : "";
174
+ runtime.log(` ${icon} ${account.id} (${status})${suffix}`);
175
+ }
102
176
  }
177
+ runtime.log(` Run 'nexus credential list' for full details`);
103
178
  }
104
179
  if (showCapabilities && capabilitySnapshot) {
105
- runtime.log(`\nCapabilities (${capabilitySnapshot.summary.active}/${capabilitySnapshot.summary.total} active)`);
106
- for (const [category, entries] of Object.entries(capabilitySnapshot.registry.categories)) {
180
+ runtime.log(`\n🎯 Capabilities (${capabilitySnapshot.summary.active}/${capabilitySnapshot.summary.total} active)`);
181
+ for (const [category, _entries] of Object.entries(capabilitySnapshot.registry.categories)) {
107
182
  const categoryCaps = capabilitySnapshot.capabilities.filter((cap) => cap.category === category);
108
183
  const activeCount = categoryCaps.filter((cap) => cap.status === "active").length;
109
184
  if (categoryCaps.length === 0)
@@ -116,16 +191,59 @@ export async function statusCommand(opts, runtime = defaultRuntime) {
116
191
  }
117
192
  }
118
193
  }
194
+ if (showUsage && usage) {
195
+ runtime.log("\n📊 Usage (last 7 days)");
196
+ const mostUsed = usage.topUsed && usage.topUsed.length > 0
197
+ ? usage.topUsed
198
+ .map((item) => `${item.name} (${item.runs})`)
199
+ .join(", ")
200
+ : usage.mostUsed
201
+ ? `${usage.mostUsed.name} (${usage.mostUsed.runs})`
202
+ : "none";
203
+ const readyUnused = usage.readyButUnused && usage.readyButUnused.length > 0
204
+ ? usage.readyButUnused.join(", ")
205
+ : "none";
206
+ runtime.log(` Most used: ${mostUsed}`);
207
+ runtime.log(` Ready but unused: ${readyUnused}`);
208
+ }
119
209
  if (suggestions.length > 0) {
120
- runtime.log("\nSuggestions");
210
+ runtime.log("\n🎯 Suggestions");
121
211
  for (const suggestion of suggestions) {
122
- runtime.log(` ${suggestion.index}. ${suggestion.id} (${suggestion.status})`);
212
+ runtime.log(` ${suggestion.index}. ${suggestion.title}`);
213
+ runtime.log(` → ${suggestion.action}`);
214
+ }
215
+ }
216
+ if (questState?.power) {
217
+ const width = 10;
218
+ const filled = Math.round((questState.power.percent / 100) * width);
219
+ const bar = `${"#".repeat(filled)}${"-".repeat(width - filled)}`;
220
+ const lines = [
221
+ `Power Level: ${bar} ${questState.power.percent}%`,
222
+ questState.power.nextQuest
223
+ ? `Next unlock: ${questState.power.nextQuest.title} (+${questState.power.nextQuest.weight}%)`
224
+ : "Next unlock: none",
225
+ "Complete the core path to reach 100%",
226
+ ];
227
+ runtime.log("");
228
+ for (const line of renderBox(lines, POWER_BOX_WIDTH)) {
229
+ runtime.log(line);
123
230
  }
124
231
  }
125
232
  runtime.log(`\nNext: run 'nexus capabilities' or 'nexus credential list' for details.`);
126
233
  }
127
234
  export async function getStatusSummary() {
128
- const identity = getIdentitySnapshot();
235
+ const resolution = resolveIdentitySnapshot();
236
+ if (!resolution.ok) {
237
+ return {
238
+ ok: false,
239
+ platform: `${process.platform}/${process.arch}`,
240
+ identity: {
241
+ agentId: "unknown",
242
+ hasIdentity: false,
243
+ },
244
+ };
245
+ }
246
+ const identity = resolution.snapshot;
129
247
  return {
130
248
  ok: true,
131
249
  platform: `${process.platform}/${process.arch}`,
@@ -1,5 +1,5 @@
1
- import { defaultRuntime } from "../runtime.js";
2
1
  import { buildUsageSuggestions } from "../infra/usage-suggestions.js";
2
+ import { defaultRuntime } from "../runtime.js";
3
3
  function parseKinds(raw) {
4
4
  if (!raw)
5
5
  return undefined;
@@ -0,0 +1,32 @@
1
+ import { fetchUsageTrackingSettings, updateUsageTrackingSettings, } from "../infra/usage-settings.js";
2
+ import { defaultRuntime } from "../runtime.js";
3
+ export async function usageTrackingCommand(opts, runtime = defaultRuntime) {
4
+ if (opts.enable && opts.disable) {
5
+ runtime.error("Choose either --enable or --disable.");
6
+ runtime.exit(1);
7
+ }
8
+ const requestedOptOut = opts.disable ? true : opts.enable ? false : null;
9
+ const result = requestedOptOut === null
10
+ ? await fetchUsageTrackingSettings()
11
+ : await updateUsageTrackingSettings(requestedOptOut);
12
+ if (!result) {
13
+ runtime.error("Usage tracking settings are unavailable (missing URL or token).");
14
+ runtime.exit(1);
15
+ }
16
+ if (opts.json) {
17
+ runtime.log(JSON.stringify(result, null, 2));
18
+ return;
19
+ }
20
+ if (!result.ok) {
21
+ const errorMessage = result.error === "not_allowed"
22
+ ? "Usage tracking is required for free accounts."
23
+ : result.error || "Failed to update usage tracking settings.";
24
+ runtime.error(errorMessage);
25
+ runtime.exit(1);
26
+ }
27
+ if (!result.canOptOut) {
28
+ runtime.log("Usage tracking is required for free accounts.");
29
+ return;
30
+ }
31
+ runtime.log(`Usage tracking ${result.optOut ? "disabled" : "enabled"}${result.planName ? ` (${result.planName})` : ""}`);
32
+ }
@@ -1,5 +1,5 @@
1
- import { defaultRuntime } from "../runtime.js";
2
1
  import { runUsageUpload } from "../infra/usage-upload.js";
2
+ import { defaultRuntime } from "../runtime.js";
3
3
  export async function usageUploadCommand(opts, runtime = defaultRuntime) {
4
4
  const result = await runUsageUpload({
5
5
  limit: opts.limit,
@@ -23,5 +23,10 @@ export async function usageUploadCommand(opts, runtime = defaultRuntime) {
23
23
  runtime.log(` uploaded: ${result.uploaded ?? 0}`);
24
24
  return;
25
25
  }
26
+ if (result.status === "disabled") {
27
+ runtime.log(`\nUsage upload disabled`);
28
+ runtime.log(` cleared_outbox: ${result.cleared ?? 0}`);
29
+ return;
30
+ }
26
31
  runtime.log("\nUsage upload complete");
27
32
  }
@@ -99,9 +99,7 @@ export function applyModelDefaults(cfg) {
99
99
  if (Object.keys(existingModels).length === 0)
100
100
  return cfg;
101
101
  let mutated = false;
102
- const nextModels = {
103
- ...existingModels,
104
- };
102
+ const nextModels = { ...existingModels };
105
103
  for (const [alias, target] of Object.entries(DEFAULT_MODEL_ALIASES)) {
106
104
  const entry = nextModels[target];
107
105
  if (!entry)
@@ -4,7 +4,7 @@
4
4
  * @example
5
5
  * ```json5
6
6
  * {
7
- * "$include": "./base.json5", // single file
7
+ * "$include": "./base.json5", // single file
8
8
  * "$include": ["./a.json5", "./b.json5"] // merge multiple
9
9
  * }
10
10
  * ```
@@ -52,7 +52,8 @@ export function deepMerge(target, source) {
52
52
  if (isPlainObject(target) && isPlainObject(source)) {
53
53
  const result = { ...target };
54
54
  for (const key of Object.keys(source)) {
55
- result[key] = key in result ? deepMerge(result[key], source[key]) : source[key];
55
+ result[key] =
56
+ key in result ? deepMerge(result[key], source[key]) : source[key];
56
57
  }
57
58
  return result;
58
59
  }
@@ -92,14 +93,11 @@ class IncludeProcessor {
92
93
  }
93
94
  processInclude(obj) {
94
95
  const includeValue = obj[INCLUDE_KEY];
95
- const otherKeys = Object.keys(obj).filter((k) => k !== INCLUDE_KEY);
96
+ const otherKeys = Object.keys(obj).filter((key) => key !== INCLUDE_KEY);
96
97
  const included = this.resolveInclude(includeValue);
97
98
  if (otherKeys.length === 0) {
98
99
  return included;
99
100
  }
100
- if (!isPlainObject(included)) {
101
- throw new ConfigIncludeError("Sibling keys require included content to be an object", typeof includeValue === "string" ? includeValue : INCLUDE_KEY);
102
- }
103
101
  // Merge included content with sibling keys
104
102
  const rest = {};
105
103
  for (const key of otherKeys) {
@@ -172,7 +170,7 @@ class IncludeProcessor {
172
170
  // Public API
173
171
  // ============================================================================
174
172
  const defaultResolver = {
175
- readFile: (p) => fs.readFileSync(p, "utf-8"),
173
+ readFile: (filePath) => fs.readFileSync(filePath, "utf-8"),
176
174
  parseJson: (raw) => JSON5.parse(raw),
177
175
  };
178
176
  /**
package/dist/config/io.js CHANGED
@@ -4,10 +4,12 @@ import path from "node:path";
4
4
  import JSON5 from "json5";
5
5
  import { loadShellEnvFallback, resolveShellEnvFallbackTimeoutMs, shouldEnableShellEnvFallback, } from "../infra/shell-env.js";
6
6
  import { applyIdentityDefaults, applyLoggingDefaults, applyMessageDefaults, applyModelDefaults, applySessionDefaults, applyTalkApiKey, } from "./defaults.js";
7
+ import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
7
8
  import { findLegacyConfigIssues } from "./legacy.js";
8
- import { CONFIG_PATH_NEXUS, resolveConfigPath, resolveStateDir, } from "./paths.js";
9
+ import { migrateLegacyConfig } from "./legacy-migrate.js";
10
+ import { resolveConfigPath, resolveStateDir } from "./paths.js";
9
11
  import { validateConfigObject } from "./validation.js";
10
- import { NexusSchema } from "./zod-schema.js";
12
+ export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
11
13
  const SHELL_ENV_EXPECTED_KEYS = [
12
14
  "OPENAI_API_KEY",
13
15
  "ANTHROPIC_API_KEY",
@@ -23,6 +25,22 @@ const SHELL_ENV_EXPECTED_KEYS = [
23
25
  "NEXUS_GATEWAY_TOKEN",
24
26
  "NEXUS_GATEWAY_PASSWORD",
25
27
  ];
28
+ function attachChannelAliases(cfg) {
29
+ const channels = cfg.channels ?? {};
30
+ const nextChannels = {
31
+ ...channels,
32
+ whatsapp: channels.whatsapp ?? cfg.whatsapp,
33
+ telegram: channels.telegram ?? cfg.telegram,
34
+ discord: channels.discord ?? cfg.discord,
35
+ slack: channels.slack ?? cfg.slack,
36
+ signal: channels.signal ?? cfg.signal,
37
+ imessage: channels.imessage ?? cfg.imessage,
38
+ };
39
+ const hasAny = Object.values(nextChannels).some((value) => value !== undefined);
40
+ if (!hasAny)
41
+ return cfg;
42
+ return { ...cfg, channels: nextChannels };
43
+ }
26
44
  function resolveConfigPathForDeps(deps) {
27
45
  if (deps.configPath)
28
46
  return deps.configPath;
@@ -48,8 +66,9 @@ export function parseConfigJson5(raw, json5 = JSON5) {
48
66
  }
49
67
  export function createConfigIO(overrides = {}) {
50
68
  const deps = normalizeDeps(overrides);
51
- const configPath = resolveConfigPathForDeps(deps);
69
+ const resolveConfigPath = () => resolveConfigPathForDeps(deps);
52
70
  function loadConfig() {
71
+ const configPath = resolveConfigPath();
53
72
  try {
54
73
  if (!deps.fs.existsSync(configPath)) {
55
74
  if (shouldEnableShellEnvFallback(deps.env)) {
@@ -65,17 +84,33 @@ export function createConfigIO(overrides = {}) {
65
84
  }
66
85
  const raw = deps.fs.readFileSync(configPath, "utf-8");
67
86
  const parsed = deps.json5.parse(raw);
68
- if (typeof parsed !== "object" || parsed === null)
87
+ const resolved = resolveConfigIncludes(parsed, configPath, {
88
+ readFile: (p) => deps.fs.readFileSync(p, "utf-8"),
89
+ parseJson: (json) => deps.json5.parse(json),
90
+ });
91
+ if (typeof resolved !== "object" || resolved === null)
69
92
  return {};
70
- const validated = NexusSchema.safeParse(parsed);
71
- if (!validated.success) {
93
+ const migrated = migrateLegacyConfig(resolved);
94
+ const candidate = migrated.config ?? resolved;
95
+ const validated = validateConfigObject(candidate);
96
+ if (!validated.ok) {
72
97
  deps.logger.error("Invalid config:");
73
- for (const iss of validated.error.issues) {
74
- deps.logger.error(`- ${iss.path.join(".")}: ${iss.message}`);
98
+ for (const iss of validated.issues) {
99
+ deps.logger.error(`- ${iss.path}: ${iss.message}`);
75
100
  }
76
101
  return {};
77
102
  }
78
- const cfg = applyModelDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(applyIdentityDefaults(validated.data)))));
103
+ if (migrated.config && migrated.changes.length > 0) {
104
+ deps.logger.warn(`Auto-migrated config: ${migrated.changes.join(" ")}`);
105
+ try {
106
+ deps.fs.writeFileSync(configPath, `${JSON.stringify(migrated.config, null, 2)}\n`);
107
+ }
108
+ catch (err) {
109
+ deps.logger.warn(`Failed to write migrated config: ${String(err)}`);
110
+ }
111
+ }
112
+ const cfg = applyModelDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(applyIdentityDefaults(validated.config)))));
113
+ const normalized = attachChannelAliases(cfg);
79
114
  const enabled = shouldEnableShellEnvFallback(deps.env) ||
80
115
  cfg.env?.shellEnv?.enabled === true;
81
116
  if (enabled) {
@@ -88,7 +123,7 @@ export function createConfigIO(overrides = {}) {
88
123
  resolveShellEnvFallbackTimeoutMs(deps.env),
89
124
  });
90
125
  }
91
- return cfg;
126
+ return normalized;
92
127
  }
93
128
  catch (err) {
94
129
  deps.logger.error(`Failed to read config at ${configPath}`, err);
@@ -96,9 +131,10 @@ export function createConfigIO(overrides = {}) {
96
131
  }
97
132
  }
98
133
  async function readConfigFileSnapshot() {
134
+ const configPath = resolveConfigPath();
99
135
  const exists = deps.fs.existsSync(configPath);
100
136
  if (!exists) {
101
- const config = applyTalkApiKey(applyModelDefaults(applySessionDefaults(applyMessageDefaults({}))));
137
+ const config = attachChannelAliases(applyTalkApiKey(applyModelDefaults(applySessionDefaults(applyMessageDefaults({})))));
102
138
  const legacyIssues = [];
103
139
  return {
104
140
  path: configPath,
@@ -128,8 +164,34 @@ export function createConfigIO(overrides = {}) {
128
164
  legacyIssues: [],
129
165
  };
130
166
  }
131
- const legacyIssues = findLegacyConfigIssues(parsedRes.parsed);
132
- const validated = validateConfigObject(parsedRes.parsed);
167
+ let resolved;
168
+ try {
169
+ resolved = resolveConfigIncludes(parsedRes.parsed, configPath, {
170
+ readFile: (p) => deps.fs.readFileSync(p, "utf-8"),
171
+ parseJson: (json) => deps.json5.parse(json),
172
+ });
173
+ }
174
+ catch (err) {
175
+ const message = err instanceof ConfigIncludeError
176
+ ? err.message
177
+ : `Include resolution failed: ${String(err)}`;
178
+ return {
179
+ path: configPath,
180
+ exists: true,
181
+ raw,
182
+ parsed: parsedRes.parsed,
183
+ valid: false,
184
+ config: {},
185
+ issues: [{ path: "", message }],
186
+ legacyIssues: [],
187
+ };
188
+ }
189
+ const migrated = migrateLegacyConfig(resolved);
190
+ const candidate = migrated.config ?? resolved;
191
+ const legacyIssues = migrated.config
192
+ ? []
193
+ : findLegacyConfigIssues(candidate);
194
+ const validated = validateConfigObject(candidate);
133
195
  if (!validated.ok) {
134
196
  return {
135
197
  path: configPath,
@@ -142,13 +204,22 @@ export function createConfigIO(overrides = {}) {
142
204
  legacyIssues,
143
205
  };
144
206
  }
207
+ if (migrated.config && migrated.changes.length > 0) {
208
+ deps.logger.warn(`Auto-migrated config: ${migrated.changes.join(" ")}`);
209
+ try {
210
+ deps.fs.writeFileSync(configPath, `${JSON.stringify(migrated.config, null, 2)}\n`);
211
+ }
212
+ catch (err) {
213
+ deps.logger.warn(`Failed to write migrated config: ${String(err)}`);
214
+ }
215
+ }
145
216
  return {
146
217
  path: configPath,
147
218
  exists: true,
148
219
  raw,
149
220
  parsed: parsedRes.parsed,
150
221
  valid: true,
151
- config: applyTalkApiKey(applyModelDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))))),
222
+ config: attachChannelAliases(applyTalkApiKey(applyModelDefaults(applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config)))))),
152
223
  issues: [],
153
224
  legacyIssues,
154
225
  };
@@ -167,6 +238,7 @@ export function createConfigIO(overrides = {}) {
167
238
  }
168
239
  }
169
240
  async function writeConfigFile(cfg) {
241
+ const configPath = resolveConfigPath();
170
242
  await deps.fs.promises.mkdir(path.dirname(configPath), {
171
243
  recursive: true,
172
244
  });
@@ -176,13 +248,13 @@ export function createConfigIO(overrides = {}) {
176
248
  await deps.fs.promises.writeFile(configPath, json, "utf-8");
177
249
  }
178
250
  return {
179
- configPath,
251
+ configPath: resolveConfigPath(),
180
252
  loadConfig,
181
253
  readConfigFileSnapshot,
182
254
  writeConfigFile,
183
255
  };
184
256
  }
185
- const defaultIO = createConfigIO({ configPath: CONFIG_PATH_NEXUS });
257
+ const defaultIO = createConfigIO();
186
258
  export const loadConfig = defaultIO.loadConfig;
187
259
  export const readConfigFileSnapshot = defaultIO.readConfigFileSnapshot;
188
260
  export const writeConfigFile = defaultIO.writeConfigFile;
@@ -138,7 +138,8 @@ const LEGACY_CONFIG_MIGRATIONS = [
138
138
  if (!match || typeof match !== "object")
139
139
  continue;
140
140
  const matchRecord = match;
141
- if (matchRecord.channel === undefined && matchRecord.provider !== undefined) {
141
+ if (matchRecord.channel === undefined &&
142
+ matchRecord.provider !== undefined) {
142
143
  matchRecord.channel = matchRecord.provider;
143
144
  delete matchRecord.provider;
144
145
  touched = true;
@@ -202,7 +203,8 @@ const LEGACY_CONFIG_MIGRATIONS = [
202
203
  if (!match || typeof match !== "object")
203
204
  continue;
204
205
  const matchRecord = match;
205
- if (matchRecord.channel === undefined && matchRecord.provider !== undefined) {
206
+ if (matchRecord.channel === undefined &&
207
+ matchRecord.provider !== undefined) {
206
208
  matchRecord.channel = matchRecord.provider;
207
209
  delete matchRecord.provider;
208
210
  touched = true;
@@ -20,6 +20,12 @@ export function resolveStateDir(env = process.env, homedir = os.homedir) {
20
20
  const override = env.NEXUS_STATE_DIR?.trim();
21
21
  if (override)
22
22
  return resolveUserPath(override);
23
+ const envHome = env.HOME?.trim();
24
+ if (envHome)
25
+ return path.join(resolveUserPath(envHome), "nexus", "state");
26
+ const envProfile = env.USERPROFILE?.trim();
27
+ if (envProfile)
28
+ return path.join(resolveUserPath(envProfile), "nexus", "state");
23
29
  return path.join(homedir(), "nexus", "state");
24
30
  }
25
31
  function resolveUserPath(input) {
@@ -54,6 +60,16 @@ export function resolveCredentialsDir(env = process.env, stateDir = resolveState
54
60
  return resolveUserPath(override);
55
61
  return path.join(stateDir, "credentials");
56
62
  }
63
+ /**
64
+ * Bootstrap prompt path (shared across agents).
65
+ * Default: $NEXUS_STATE_DIR/agents/BOOTSTRAP.md
66
+ */
67
+ export function resolveBootstrapPath(env = process.env, stateDir = resolveStateDir(env, os.homedir)) {
68
+ const override = env.NEXUS_BOOTSTRAP_PATH?.trim();
69
+ if (override)
70
+ return resolveUserPath(override);
71
+ return path.join(stateDir, "agents", "BOOTSTRAP.md");
72
+ }
57
73
  const OAUTH_FILENAME = "oauth.json";
58
74
  /** @deprecated Use resolveCredentialsDir. */
59
75
  export function resolveOAuthDir(env = process.env, stateDir = resolveStateDir(env, os.homedir)) {