@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,9 +1,9 @@
1
- import { normalizeElevatedLevel, normalizeThinkLevel, normalizeVerboseLevel, } from "../thinking.js";
1
+ import { normalizeElevatedLevel, normalizeReasoningLevel, normalizeThinkLevel, normalizeVerboseLevel, } from "../thinking.js";
2
2
  export function extractThinkDirective(body) {
3
3
  if (!body)
4
4
  return { cleaned: "", hasDirective: false };
5
5
  // Match the longest keyword first to avoid partial captures (e.g. "/think:high")
6
- const match = body.match(/(?:^|\s)\/(?:thinking|think|t)\s*:?\s*([a-zA-Z-]+)\b/i);
6
+ const match = body.match(/(?:^|\s)\/(?:thinking|think|t)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)?\b/i);
7
7
  const thinkLevel = normalizeThinkLevel(match?.[1]);
8
8
  const cleaned = match
9
9
  ? body.replace(match[0], "").replace(/\s+/g, " ").trim()
@@ -45,6 +45,21 @@ export function extractElevatedDirective(body) {
45
45
  hasDirective: !!match,
46
46
  };
47
47
  }
48
+ export function extractReasoningDirective(body) {
49
+ if (!body)
50
+ return { cleaned: "", hasDirective: false };
51
+ const match = body.match(/(?:^|\s)\/(?:reasoning|reason)(?=$|\s|:)\s*:?\s*([a-zA-Z-]+)?\b/i);
52
+ const reasoningLevel = normalizeReasoningLevel(match?.[1]);
53
+ const cleaned = match
54
+ ? body.replace(match[0], "").replace(/\s+/g, " ").trim()
55
+ : body.trim();
56
+ return {
57
+ cleaned,
58
+ reasoningLevel,
59
+ rawLevel: match?.[1],
60
+ hasDirective: !!match,
61
+ };
62
+ }
48
63
  export function extractStatusDirective(body) {
49
64
  if (!body)
50
65
  return { cleaned: "", hasDirective: false };
@@ -122,7 +122,8 @@ export function resolveModelDirectiveSelection(params) {
122
122
  if (params.provider && provider !== normalizeProviderId(params.provider))
123
123
  continue;
124
124
  const haystack = `${provider}/${model}`.toLowerCase();
125
- if (haystack.includes(fragment) || model.toLowerCase().includes(fragment)) {
125
+ if (haystack.includes(fragment) ||
126
+ model.toLowerCase().includes(fragment)) {
126
127
  candidates.push({ provider, model });
127
128
  }
128
129
  }
@@ -132,7 +133,10 @@ export function resolveModelDirectiveSelection(params) {
132
133
  for (const [aliasKey, entry] of aliasIndex.byAlias.entries()) {
133
134
  if (!aliasKey.includes(fragment))
134
135
  continue;
135
- aliasMatches.push({ provider: entry.ref.provider, model: entry.ref.model });
136
+ aliasMatches.push({
137
+ provider: entry.ref.provider,
138
+ model: entry.ref.model,
139
+ });
136
140
  }
137
141
  for (const match of aliasMatches) {
138
142
  const key = modelKey(match.provider, match.model);
@@ -176,7 +180,8 @@ export function resolveModelDirectiveSelection(params) {
176
180
  }
177
181
  const resolvedKey = modelKey(resolved.ref.provider, resolved.ref.model);
178
182
  if (allowedModelKeys.size === 0 || allowedModelKeys.has(resolvedKey)) {
179
- const alias = resolved.alias ?? pickAliasForKey(resolved.ref.provider, resolved.ref.model);
183
+ const alias = resolved.alias ??
184
+ pickAliasForKey(resolved.ref.provider, resolved.ref.model);
180
185
  return {
181
186
  selection: {
182
187
  provider: resolved.ref.provider,
@@ -370,8 +370,8 @@ function defaultQueueModeForProvider(provider) {
370
370
  export function resolveQueueSettings(params) {
371
371
  const providerKey = params.provider?.trim().toLowerCase();
372
372
  const queueCfg = params.cfg.routing?.queue;
373
- const providerModeRaw = providerKey && queueCfg?.byProvider
374
- ? queueCfg.byProvider[providerKey]
373
+ const providerModeRaw = providerKey && queueCfg?.byChannel
374
+ ? queueCfg.byChannel[providerKey]
375
375
  : undefined;
376
376
  const resolvedMode = params.inlineMode ??
377
377
  normalizeQueueMode(params.sessionEntry?.queueMode) ??
@@ -35,7 +35,7 @@ export { extractElevatedDirective, extractThinkDirective, extractVerboseDirectiv
35
35
  export { extractQueueDirective } from "./reply/queue.js";
36
36
  export { extractReplyToTag } from "./reply/reply-tags.js";
37
37
  const BARE_SESSION_RESET_PROMPT = "A new session was started via /new or /reset. Say hi briefly (1-2 sentences) and ask what the user wants to do next. Do not mention internal steps, files, tools, or reasoning.";
38
- const CONTROL_COMMAND_PREFIX_RE = /^\/(?:status|help|thinking|think|t|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new|compact)\b/i;
38
+ const CONTROL_COMMAND_PREFIX_RE = /^\/(?:status|help|thinking|think|t|reasoning|reason|verbose|v|elevated|elev|model|queue|activation|send|restart|reset|new|compact)\b/i;
39
39
  function normalizeAllowToken(value) {
40
40
  if (!value)
41
41
  return "";
@@ -21,6 +21,8 @@ export function normalizeThinkLevel(raw) {
21
21
  "max",
22
22
  ].includes(key))
23
23
  return "high";
24
+ if (["xhigh", "x-high", "extra-high"].includes(key))
25
+ return "xhigh";
24
26
  if (["think"].includes(key))
25
27
  return "minimal";
26
28
  return undefined;
@@ -47,3 +49,16 @@ export function normalizeElevatedLevel(raw) {
47
49
  return "on";
48
50
  return undefined;
49
51
  }
52
+ // Normalize reasoning visibility flags used to control <think> output.
53
+ export function normalizeReasoningLevel(raw) {
54
+ if (!raw)
55
+ return undefined;
56
+ const key = raw.toLowerCase();
57
+ if (["off", "false", "no", "0"].includes(key))
58
+ return "off";
59
+ if (["on", "true", "yes", "1"].includes(key))
60
+ return "on";
61
+ if (["stream", "live"].includes(key))
62
+ return "stream";
63
+ return undefined;
64
+ }
@@ -7,7 +7,7 @@ import { ensurePortAvailable } from "../infra/ports.js";
7
7
  import { createSubsystemLogger } from "../logging.js";
8
8
  import { CONFIG_DIR } from "../utils.js";
9
9
  import { normalizeCdpWsUrl } from "./cdp.js";
10
- import { DEFAULT_NEXUS_BROWSER_COLOR, DEFAULT_NEXUS_BROWSER_PROFILE_NAME } from "./constants.js";
10
+ import { DEFAULT_NEXUS_BROWSER_COLOR, DEFAULT_NEXUS_BROWSER_PROFILE_NAME, } from "./constants.js";
11
11
  const log = createSubsystemLogger("browser").child("chrome");
12
12
  function exists(filePath) {
13
13
  try {
@@ -96,6 +96,8 @@ export async function browserSnapshot(baseUrl, opts) {
96
96
  q.set("targetId", opts.targetId);
97
97
  if (typeof opts.limit === "number")
98
98
  q.set("limit", String(opts.limit));
99
+ if (typeof opts.maxChars === "number")
100
+ q.set("maxChars", String(opts.maxChars));
99
101
  if (opts.profile)
100
102
  q.set("profile", opts.profile);
101
103
  return await fetchBrowserJson(`${baseUrl}/snapshot?${q.toString()}`, {
@@ -56,7 +56,8 @@ function ensureDefaultProfile(profiles, defaultColor, legacyCdpPort, derivedDefa
56
56
  }
57
57
  export function resolveBrowserConfig(cfg) {
58
58
  const enabled = cfg?.enabled ?? DEFAULT_NEXUS_BROWSER_ENABLED;
59
- const envControlUrl = (process.env.NEXUS_BROWSER_CONTROL_URL ?? process.env.NEXUS_BROWSER_CONTROL_URL)?.trim();
59
+ const envControlUrl = (process.env.NEXUS_BROWSER_CONTROL_URL ??
60
+ process.env.NEXUS_BROWSER_CONTROL_URL)?.trim();
60
61
  const derivedControlPort = (() => {
61
62
  const raw = (process.env.NEXUS_GATEWAY_PORT ?? process.env.NEXUS_GATEWAY_PORT)?.trim();
62
63
  if (!raw)
@@ -69,7 +70,10 @@ export function resolveBrowserConfig(cfg) {
69
70
  const derivedControlUrl = derivedControlPort
70
71
  ? `http://127.0.0.1:${derivedControlPort}`
71
72
  : null;
72
- const controlInfo = parseHttpUrl(cfg?.controlUrl ?? envControlUrl ?? derivedControlUrl ?? DEFAULT_NEXUS_BROWSER_CONTROL_URL, "browser.controlUrl");
73
+ const controlInfo = parseHttpUrl(cfg?.controlUrl ??
74
+ envControlUrl ??
75
+ derivedControlUrl ??
76
+ DEFAULT_NEXUS_BROWSER_CONTROL_URL, "browser.controlUrl");
73
77
  const controlPort = controlInfo.port;
74
78
  const defaultColor = normalizeHexColor(cfg?.color);
75
79
  const derivedCdpRange = deriveDefaultBrowserCdpPortRange(controlPort);
@@ -20,6 +20,9 @@ export async function snapshotAiViaPlaywright(opts) {
20
20
  const result = await maybe._snapshotForAI({
21
21
  timeout: Math.max(500, Math.min(60_000, Math.floor(opts.timeoutMs ?? 5000))),
22
22
  track: "response",
23
+ ...(typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars)
24
+ ? { maxChars: Math.max(1, Math.floor(opts.maxChars)) }
25
+ : {}),
23
26
  });
24
27
  return { snapshot: String(result?.full ?? "") };
25
28
  }
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { ensureMediaDir, saveMediaBuffer } from "../../media/store.js";
3
3
  import { captureScreenshot, snapshotAria } from "../cdp.js";
4
+ import { DEFAULT_AI_SNAPSHOT_MAX_CHARS } from "../constants.js";
4
5
  import { DEFAULT_BROWSER_SCREENSHOT_MAX_BYTES, DEFAULT_BROWSER_SCREENSHOT_MAX_SIDE, normalizeBrowserScreenshot, } from "../screenshot.js";
5
6
  import { getProfileContext, jsonError, toBoolean, toNumber, toStringArray, toStringOrEmpty, } from "./utils.js";
6
7
  const SELECTOR_UNSUPPORTED_MESSAGE = [
@@ -498,6 +499,18 @@ export function registerBrowserAgentRoutes(app, ctx) {
498
499
  ? "ai"
499
500
  : "aria";
500
501
  const limit = typeof req.query.limit === "string" ? Number(req.query.limit) : undefined;
502
+ const maxCharsRaw = typeof req.query.maxChars === "string" ||
503
+ typeof req.query.maxChars === "number"
504
+ ? Number(req.query.maxChars)
505
+ : undefined;
506
+ const maxChars = format === "ai" &&
507
+ typeof maxCharsRaw === "number" &&
508
+ Number.isFinite(maxCharsRaw) &&
509
+ maxCharsRaw > 0
510
+ ? Math.floor(maxCharsRaw)
511
+ : format === "ai"
512
+ ? DEFAULT_AI_SNAPSHOT_MAX_CHARS
513
+ : undefined;
501
514
  try {
502
515
  const tab = await profileCtx.ensureTabAvailable(targetId || undefined);
503
516
  if (format === "ai") {
@@ -507,6 +520,7 @@ export function registerBrowserAgentRoutes(app, ctx) {
507
520
  const snap = await pw.snapshotAiViaPlaywright({
508
521
  cdpUrl: profileCtx.profile.cdpUrl,
509
522
  targetId: tab.targetId,
523
+ maxChars,
510
524
  });
511
525
  return res.json({
512
526
  ok: true,
@@ -126,7 +126,7 @@ async function resolveFilePath(rootReal, urlPath) {
126
126
  }
127
127
  }
128
128
  function isDisabledByEnv() {
129
- if (process.env.NEXUS_SKIP_CANVAS_HOST === "1" || process.env.NEXUS_SKIP_CANVAS_HOST === "1")
129
+ if (process.env.NEXUS_SKIP_CANVAS_HOST === "1")
130
130
  return true;
131
131
  if (process.env.NODE_ENV === "test")
132
132
  return true;
@@ -0,0 +1,245 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { getSkillMetadata, isConfigPathTruthy, loadWorkspaceSkillEntries, resolveRuntimePlatform, resolveSkillConfig, } from "../agents/skills.js";
4
+ import { loadConfig } from "../config/config.js";
5
+ import { ensureCredentialIndexSync } from "../credentials/store.js";
6
+ import { MANAGED_SKILLS_DIR, NEXUS_ROOT } from "../utils.js";
7
+ import { loadCapabilityRegistry } from "./registry.js";
8
+ const STATUS_PRIORITY = [
9
+ "active",
10
+ "ready",
11
+ "needs_setup",
12
+ "needs_install",
13
+ "broken",
14
+ "unavailable",
15
+ ];
16
+ function hasBinary(bin) {
17
+ const pathEnv = process.env.PATH ?? "";
18
+ const parts = pathEnv.split(path.delimiter).filter(Boolean);
19
+ for (const part of parts) {
20
+ const candidate = path.join(part, bin);
21
+ try {
22
+ fs.accessSync(candidate, fs.constants.X_OK);
23
+ return true;
24
+ }
25
+ catch {
26
+ // keep scanning
27
+ }
28
+ }
29
+ return false;
30
+ }
31
+ function normalizeProviderId(raw) {
32
+ return raw.trim().toLowerCase();
33
+ }
34
+ function resolveCredentialAliases(providerId) {
35
+ const base = providerId.replace(/-oauth$/, "");
36
+ const aliases = new Set([providerId, base]);
37
+ if (providerId === "gog") {
38
+ aliases.add("google");
39
+ aliases.add("google-oauth");
40
+ }
41
+ if (providerId === "google-oauth") {
42
+ aliases.add("google");
43
+ aliases.add("gog");
44
+ }
45
+ return Array.from(aliases).filter(Boolean);
46
+ }
47
+ function setupLooksCredentialed(raw) {
48
+ const lowered = raw.toLowerCase();
49
+ if (!lowered || lowered === "-" || lowered === "none")
50
+ return false;
51
+ return (lowered.includes("oauth") ||
52
+ lowered.includes("api key") ||
53
+ lowered.includes("apikey") ||
54
+ lowered.includes("token") ||
55
+ lowered.includes("account") ||
56
+ lowered.includes("bot") ||
57
+ lowered.includes("key"));
58
+ }
59
+ function detectSkillUsageActive(skillName) {
60
+ const usageLog = path.join(MANAGED_SKILLS_DIR, skillName, "usage.log");
61
+ try {
62
+ const stat = fs.statSync(usageLog);
63
+ return stat.isFile() && stat.size > 0;
64
+ }
65
+ catch {
66
+ return false;
67
+ }
68
+ }
69
+ function resolveSkillType(_skillName, metadata) {
70
+ if (metadata?.type)
71
+ return metadata.type;
72
+ const requires = metadata?.requires;
73
+ if (requires?.env?.length || requires?.config?.length)
74
+ return "connector";
75
+ if (requires?.bins?.length)
76
+ return "tool";
77
+ return "guide";
78
+ }
79
+ function resolveSkillProviderStatus(params) {
80
+ const { skillName, providerId, config, credentialServices, metadata, setupHint, } = params;
81
+ const platform = resolveRuntimePlatform();
82
+ const skillType = resolveSkillType(skillName, metadata);
83
+ if (metadata?.os?.length && !metadata.os.includes(platform)) {
84
+ return {
85
+ id: providerId,
86
+ kind: "skill",
87
+ status: "unavailable",
88
+ reason: "os",
89
+ };
90
+ }
91
+ const skillKey = metadata?.skillKey ?? skillName;
92
+ const skillConfig = resolveSkillConfig(config, skillKey);
93
+ const requires = metadata?.requires;
94
+ const missingBins = (requires?.bins ?? []).filter((bin) => !hasBinary(bin));
95
+ const anyBins = requires?.anyBins ?? [];
96
+ const anyBinsOk = anyBins.length === 0 || anyBins.some((bin) => hasBinary(bin));
97
+ if (missingBins.length > 0 || !anyBinsOk) {
98
+ return {
99
+ id: providerId,
100
+ kind: "skill",
101
+ status: "needs_install",
102
+ reason: missingBins.length > 0 ? "missing_bins" : "missing_any_bins",
103
+ };
104
+ }
105
+ const missingEnv = (requires?.env ?? []).filter((envName) => {
106
+ if (process.env[envName])
107
+ return false;
108
+ if (skillConfig?.env?.[envName])
109
+ return false;
110
+ if (skillConfig?.apiKey && metadata?.primaryEnv === envName)
111
+ return false;
112
+ return true;
113
+ });
114
+ if (missingEnv.length > 0) {
115
+ return {
116
+ id: providerId,
117
+ kind: "skill",
118
+ status: "needs_setup",
119
+ reason: "missing_env",
120
+ };
121
+ }
122
+ const missingConfig = (requires?.config ?? []).filter((configPath) => !isConfigPathTruthy(config, configPath));
123
+ if (missingConfig.length > 0) {
124
+ return {
125
+ id: providerId,
126
+ kind: "skill",
127
+ status: "needs_setup",
128
+ reason: "missing_config",
129
+ };
130
+ }
131
+ const credentialHints = requires?.credentials ?? [];
132
+ const requiresCredential = setupLooksCredentialed(setupHint ?? "");
133
+ if (skillType === "connector" ||
134
+ credentialHints.length > 0 ||
135
+ requiresCredential) {
136
+ const aliases = new Set();
137
+ for (const entry of [providerId, ...credentialHints]) {
138
+ for (const alias of resolveCredentialAliases(entry))
139
+ aliases.add(alias);
140
+ }
141
+ const hasCred = Array.from(aliases).some((alias) => credentialServices.has(alias));
142
+ if (!hasCred) {
143
+ return {
144
+ id: providerId,
145
+ kind: "skill",
146
+ status: "needs_setup",
147
+ reason: "missing_credentials",
148
+ };
149
+ }
150
+ }
151
+ if (detectSkillUsageActive(skillName)) {
152
+ return { id: providerId, kind: "skill", status: "active" };
153
+ }
154
+ return { id: providerId, kind: "skill", status: "ready" };
155
+ }
156
+ function resolveCredentialProviderStatus(providerId, credentialServices) {
157
+ const aliases = resolveCredentialAliases(providerId);
158
+ const hasCredential = aliases.some((alias) => credentialServices.has(alias));
159
+ return {
160
+ id: providerId,
161
+ kind: "credential",
162
+ status: hasCredential ? "ready" : "needs_setup",
163
+ reason: hasCredential ? undefined : "missing_credentials",
164
+ };
165
+ }
166
+ function pickBestStatus(statuses) {
167
+ for (const status of STATUS_PRIORITY) {
168
+ if (statuses.includes(status))
169
+ return status;
170
+ }
171
+ return "unavailable";
172
+ }
173
+ export function detectCapabilities(params) {
174
+ const config = params?.config ?? loadConfig();
175
+ const workspaceDir = params?.workspaceDir ?? path.join(NEXUS_ROOT, "home");
176
+ const registry = params?.registry ?? loadCapabilityRegistry();
177
+ const credentialIndex = ensureCredentialIndexSync();
178
+ const credentialServices = new Set(Object.keys(credentialIndex.services ?? {}).map((name) => normalizeProviderId(name)));
179
+ const skillEntries = loadWorkspaceSkillEntries(workspaceDir, { config });
180
+ const skillMap = new Map();
181
+ const providesMap = new Map();
182
+ for (const entry of skillEntries) {
183
+ const metadata = getSkillMetadata(entry);
184
+ const key = normalizeProviderId(metadata?.skillKey ?? entry.skill.name);
185
+ skillMap.set(key, { name: entry.skill.name, metadata });
186
+ const provides = (metadata?.provides ?? [])
187
+ .map((cap) => cap.trim())
188
+ .filter(Boolean);
189
+ for (const capId of provides) {
190
+ const normalizedCap = capId.trim();
191
+ if (!normalizedCap)
192
+ continue;
193
+ const existing = providesMap.get(normalizedCap) ?? [];
194
+ if (!existing.includes(key))
195
+ existing.push(key);
196
+ providesMap.set(normalizedCap, existing);
197
+ }
198
+ }
199
+ const capabilities = [];
200
+ for (const capability of registry.all) {
201
+ const extraProviders = providesMap.get(capability.id) ?? [];
202
+ const providerList = Array.from(new Set([
203
+ ...capability.providers.map(normalizeProviderId),
204
+ ...extraProviders,
205
+ ]));
206
+ const providerStatuses = [];
207
+ for (const provider of providerList) {
208
+ const normalized = normalizeProviderId(provider);
209
+ const skillEntry = skillMap.get(normalized);
210
+ if (skillEntry) {
211
+ providerStatuses.push(resolveSkillProviderStatus({
212
+ skillName: skillEntry.name,
213
+ providerId: normalized,
214
+ config,
215
+ credentialServices,
216
+ metadata: skillEntry.metadata,
217
+ setupHint: capability.setup,
218
+ }));
219
+ }
220
+ else if (resolveCredentialAliases(normalized).some((alias) => credentialServices.has(alias))) {
221
+ providerStatuses.push(resolveCredentialProviderStatus(normalized, credentialServices));
222
+ }
223
+ else {
224
+ providerStatuses.push({
225
+ id: normalized,
226
+ kind: "unknown",
227
+ status: "unavailable",
228
+ });
229
+ }
230
+ }
231
+ const status = pickBestStatus(providerStatuses.map((p) => p.status));
232
+ capabilities.push({ ...capability, status, providers: providerStatuses });
233
+ }
234
+ const summary = {
235
+ total: capabilities.length,
236
+ active: capabilities.filter((c) => c.status === "active").length,
237
+ ready: capabilities.filter((c) => c.status === "ready").length,
238
+ needs_setup: capabilities.filter((c) => c.status === "needs_setup").length,
239
+ needs_install: capabilities.filter((c) => c.status === "needs_install")
240
+ .length,
241
+ unavailable: capabilities.filter((c) => c.status === "unavailable").length,
242
+ broken: capabilities.filter((c) => c.status === "broken").length,
243
+ };
244
+ return { registry, capabilities, summary };
245
+ }
@@ -0,0 +1,99 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { resolveUserPath } from "../utils.js";
5
+ const CAPABILITIES_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/CAPABILITIES.md");
6
+ function resolveCapabilitiesPath() {
7
+ const candidates = [];
8
+ const override = process.env.NEXUS_CAPABILITIES_PATH?.trim();
9
+ if (override) {
10
+ candidates.push(resolveUserPath(override));
11
+ }
12
+ candidates.push(CAPABILITIES_PATH);
13
+ for (const candidate of candidates) {
14
+ if (!candidate)
15
+ continue;
16
+ try {
17
+ if (fs.existsSync(candidate))
18
+ return candidate;
19
+ }
20
+ catch {
21
+ // ignore and keep searching
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ function normalizeProviderToken(raw) {
27
+ const normalized = raw
28
+ .replace(/\(.*?\)/g, "")
29
+ .replace(/\s+/g, " ")
30
+ .trim()
31
+ .toLowerCase();
32
+ if (normalized.endsWith(" guide")) {
33
+ return normalized.slice(0, -" guide".length).trim();
34
+ }
35
+ return normalized;
36
+ }
37
+ function parseProviders(raw) {
38
+ const cleaned = raw.replace(/\(.*?\)/g, "").trim();
39
+ if (!cleaned)
40
+ return [];
41
+ const parts = cleaned.split(/[,;+]/g).map((p) => p.trim());
42
+ const out = [];
43
+ for (const part of parts) {
44
+ if (!part)
45
+ continue;
46
+ const normalized = normalizeProviderToken(part);
47
+ if (!normalized)
48
+ continue;
49
+ if (!out.includes(normalized))
50
+ out.push(normalized);
51
+ }
52
+ return out;
53
+ }
54
+ export function loadCapabilityRegistry() {
55
+ const capabilitiesPath = resolveCapabilitiesPath();
56
+ if (!capabilitiesPath) {
57
+ return { categories: {}, all: [] };
58
+ }
59
+ let raw;
60
+ try {
61
+ raw = fs.readFileSync(capabilitiesPath, "utf-8");
62
+ }
63
+ catch {
64
+ return { categories: {}, all: [] };
65
+ }
66
+ const lines = raw.replace(/\r\n/g, "\n").split("\n");
67
+ let currentCategory = "Uncategorized";
68
+ const categories = {};
69
+ const all = [];
70
+ for (const line of lines) {
71
+ const headingMatch = line.match(/^###\s+(.+)$/);
72
+ if (headingMatch) {
73
+ currentCategory =
74
+ headingMatch[1]?.replace(/^[-–]\s*/, "").trim() || currentCategory;
75
+ continue;
76
+ }
77
+ const tableMatch = line.match(/^\|\s*`([^`]+)`\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|/);
78
+ if (!tableMatch)
79
+ continue;
80
+ const [, idRaw, descriptionRaw, providersRaw, setupRaw] = tableMatch;
81
+ const id = (idRaw ?? "").trim();
82
+ if (!id)
83
+ continue;
84
+ const providersRawTrimmed = (providersRaw ?? "").trim();
85
+ const providers = parseProviders(providersRawTrimmed);
86
+ const capability = {
87
+ id,
88
+ description: (descriptionRaw ?? "").trim(),
89
+ providersRaw: providersRawTrimmed,
90
+ providers,
91
+ setup: (setupRaw ?? "").trim(),
92
+ category: currentCategory,
93
+ };
94
+ categories[currentCategory] = categories[currentCategory] ?? [];
95
+ categories[currentCategory].push(capability);
96
+ all.push(capability);
97
+ }
98
+ return { categories, all };
99
+ }
@@ -0,0 +1,44 @@
1
+ function resolveLocation(location) {
2
+ const source = location.source ??
3
+ (location.isLive ? "live" : location.name || location.address ? "place" : "pin");
4
+ const isLive = Boolean(location.isLive ?? source === "live");
5
+ return { ...location, source, isLive };
6
+ }
7
+ function formatAccuracy(accuracy) {
8
+ if (!Number.isFinite(accuracy))
9
+ return "";
10
+ return ` ±${Math.round(accuracy ?? 0)}m`;
11
+ }
12
+ function formatCoords(latitude, longitude) {
13
+ return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
14
+ }
15
+ export function formatLocationText(location) {
16
+ const resolved = resolveLocation(location);
17
+ const coords = formatCoords(resolved.latitude, resolved.longitude);
18
+ const accuracy = formatAccuracy(resolved.accuracy);
19
+ const caption = resolved.caption?.trim();
20
+ let header = "";
21
+ if (resolved.source === "live" || resolved.isLive) {
22
+ header = `🛰 Live location: ${coords}${accuracy}`;
23
+ }
24
+ else if (resolved.name || resolved.address) {
25
+ const label = [resolved.name, resolved.address].filter(Boolean).join(" — ");
26
+ header = `📍 ${label} (${coords}${accuracy})`;
27
+ }
28
+ else {
29
+ header = `📍 ${coords}${accuracy}`;
30
+ }
31
+ return caption ? `${header}\n${caption}` : header;
32
+ }
33
+ export function toLocationContext(location) {
34
+ const resolved = resolveLocation(location);
35
+ return {
36
+ LocationLat: resolved.latitude,
37
+ LocationLon: resolved.longitude,
38
+ LocationAccuracy: resolved.accuracy,
39
+ LocationName: resolved.name,
40
+ LocationAddress: resolved.address,
41
+ LocationSource: resolved.source,
42
+ LocationIsLive: resolved.isLive,
43
+ };
44
+ }
@@ -0,0 +1,2 @@
1
+ /* istanbul ignore file */
2
+ export { createWaSocket, loginWeb, logWebSelfId, monitorWebChannel, monitorWebInbox, pickWebChannel, sendMessageWhatsApp, WA_WEB_AUTH_DIR, waitForWaConnection, webAuthExists, } from "../../channel-web.js";
@@ -1,10 +1,10 @@
1
+ import { spawn } from "node:child_process";
2
+ import crypto from "node:crypto";
1
3
  import fs from "node:fs";
2
4
  import path from "node:path";
3
- import crypto from "node:crypto";
4
- import { spawn } from "node:child_process";
5
5
  import { scanCredentials } from "../commands/credential.js";
6
6
  import { openUrl } from "../commands/onboard-helpers.js";
7
- import { storeKeychainSecret, writeCredentialRecord, } from "../credentials/store.js";
7
+ import { resolveDefaultEnvVar, storeKeychainSecret, writeCredentialRecord, } from "../credentials/store.js";
8
8
  import { NEXUS_ROOT, sleep } from "../utils.js";
9
9
  const DEFAULT_CLOUD_TIMEOUT_MS = 120_000;
10
10
  const DEFAULT_CLOUD_POLL_MS = 2_000;
@@ -77,6 +77,7 @@ async function storeHubToken(params) {
77
77
  value: params.token,
78
78
  });
79
79
  }
80
+ let envVar;
80
81
  if (storedInKeychain) {
81
82
  record = {
82
83
  owner: "user",
@@ -86,17 +87,18 @@ async function storeHubToken(params) {
86
87
  };
87
88
  }
88
89
  else {
90
+ envVar = resolveDefaultEnvVar({ service, type: "token" });
91
+ process.env[envVar] = params.token;
89
92
  record = {
90
93
  owner: "user",
91
94
  type: "token",
92
95
  configuredAt: now,
93
- storage: { provider: "plaintext" },
94
- token: params.token,
96
+ storage: { provider: "env", var: envVar },
95
97
  };
96
98
  }
97
99
  await writeCredentialRecord(service, account, authId, record);
98
100
  await scanCredentials();
99
- return { storedInKeychain, account };
101
+ return { storedInKeychain, account, envVar };
100
102
  }
101
103
  function parseCloudLoginArgs(args) {
102
104
  let baseUrl = getHubBaseUrl();
@@ -296,7 +298,10 @@ async function handleCloudLogin(rawArgs) {
296
298
  }
297
299
  console.log(stored.storedInKeychain
298
300
  ? " Token stored in keychain"
299
- : " Token stored in plaintext credentials");
301
+ : ` Token stored in env var ${stored.envVar ?? ""}`.trim());
302
+ if (!stored.storedInKeychain && stored.envVar) {
303
+ console.log(` Persist it in your shell: export ${stored.envVar}=...`);
304
+ }
300
305
  if (cloudUrl) {
301
306
  console.log(` Cloud URL: ${cloudUrl}`);
302
307
  }