@intent-systems/nexus 2026.1.5-4 → 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 (123) 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 +46 -15
  35. package/dist/capabilities/registry.js +2 -1
  36. package/dist/cli/cloud-cli.js +12 -7
  37. package/dist/cli/credential-cli.js +139 -17
  38. package/dist/cli/gateway-cli.js +1 -1
  39. package/dist/cli/log-cli.js +25 -0
  40. package/dist/cli/pairing-cli.js +1 -1
  41. package/dist/cli/program.js +58 -6
  42. package/dist/cli/run-main.js +1 -1
  43. package/dist/cli/skills-cli.js +144 -21
  44. package/dist/cli/skills-hub-cli.js +59 -29
  45. package/dist/cli/tool-connector-cli.js +99 -24
  46. package/dist/cli/upstream-sync-cli.js +253 -96
  47. package/dist/cli/usage-cli.js +14 -0
  48. package/dist/commands/auth-choice-options.js +6 -1
  49. package/dist/commands/auth-choice.js +157 -5
  50. package/dist/commands/bootstrap-preset.js +10 -6
  51. package/dist/commands/capabilities.js +33 -6
  52. package/dist/commands/claude-md.js +3 -2
  53. package/dist/commands/config-view.js +1 -1
  54. package/dist/commands/configure.js +4 -4
  55. package/dist/commands/credential.js +497 -36
  56. package/dist/commands/cursor-rules.js +39 -19
  57. package/dist/commands/doctor.js +5 -4
  58. package/dist/commands/identity.js +28 -31
  59. package/dist/commands/init.js +15 -18
  60. package/dist/commands/log.js +134 -0
  61. package/dist/commands/models/fallbacks.js +1 -1
  62. package/dist/commands/models/image-fallbacks.js +1 -1
  63. package/dist/commands/models/list.js +1 -1
  64. package/dist/commands/models/scan.js +1 -1
  65. package/dist/commands/onboard-auth.js +27 -2
  66. package/dist/commands/onboard-eve-identity.js +7 -8
  67. package/dist/commands/onboard-non-interactive.js +4 -2
  68. package/dist/commands/onboard-quickstart.js +18 -11
  69. package/dist/commands/quest-state.js +271 -0
  70. package/dist/commands/quest.js +53 -13
  71. package/dist/commands/reset.js +1 -1
  72. package/dist/commands/sessions-ingest.js +5 -4
  73. package/dist/commands/setup.js +4 -2
  74. package/dist/commands/skills-manifest.js +2 -2
  75. package/dist/commands/status.js +179 -61
  76. package/dist/commands/suggestions.js +1 -1
  77. package/dist/commands/usage-tracking.js +32 -0
  78. package/dist/commands/usage-upload.js +6 -1
  79. package/dist/config/defaults.js +1 -3
  80. package/dist/config/includes.js +5 -7
  81. package/dist/config/io.js +88 -16
  82. package/dist/config/legacy.js +4 -2
  83. package/dist/config/paths.js +16 -0
  84. package/dist/config/sessions.js +9 -5
  85. package/dist/config/zod-schema.js +4 -3
  86. package/dist/control-plane/broker/broker.js +131 -78
  87. package/dist/control-plane/compaction.js +3 -5
  88. package/dist/control-plane/factory.js +2 -2
  89. package/dist/control-plane/index.js +2 -2
  90. package/dist/control-plane/odu/agents.js +28 -23
  91. package/dist/control-plane/odu/interaction-tools.js +62 -50
  92. package/dist/control-plane/odu/prompt-loader.js +8 -8
  93. package/dist/control-plane/odu/runtime.js +87 -75
  94. package/dist/control-plane/odu-control-plane.js +14 -12
  95. package/dist/control-plane/single-agent.js +13 -13
  96. package/dist/credentials/store.js +133 -7
  97. package/dist/gateway/server-browser.js +5 -4
  98. package/dist/gateway/server-methods/cron.js +11 -1
  99. package/dist/gateway/server.js +14 -7
  100. package/dist/infra/bonjour.js +1 -1
  101. package/dist/infra/event-log.js +8 -2
  102. package/dist/infra/path-env.js +1 -2
  103. package/dist/infra/provider-usage.auth.js +5 -3
  104. package/dist/infra/provider-usage.fetch.claude.js +16 -6
  105. package/dist/infra/provider-usage.fetch.minimax.js +8 -3
  106. package/dist/infra/provider-usage.js +9 -5
  107. package/dist/infra/restart.js +2 -2
  108. package/dist/infra/usage-settings.js +78 -0
  109. package/dist/infra/usage-suggestions.js +17 -5
  110. package/dist/infra/usage-upload.js +38 -1
  111. package/dist/infra/voicewake.js +2 -2
  112. package/dist/media/image-ops.js +3 -1
  113. package/dist/memory/index.js +2 -381
  114. package/dist/pairing/pairing-store.js +24 -0
  115. package/dist/providers/github-copilot-auth.js +1 -1
  116. package/dist/routing/resolve-route.js +6 -6
  117. package/dist/routing/session-key.js +3 -1
  118. package/dist/sessions/send-policy.js +5 -5
  119. package/dist/slack/monitor.js +22 -1
  120. package/dist/telegram/reaction-level.js +2 -1
  121. package/dist/utils.js +4 -3
  122. package/dist/wizard/onboarding.js +29 -7
  123. package/package.json +1 -1
@@ -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;
@@ -1,10 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { getSkillMetadata, isConfigPathTruthy, loadWorkspaceSkillEntries, resolveRuntimePlatform, resolveSkillConfig, } from "../agents/skills.js";
3
4
  import { loadConfig } from "../config/config.js";
4
- import { resolveStateDir } from "../config/paths.js";
5
- import { getSkillMetadata, isConfigPathTruthy, loadWorkspaceSkillEntries, resolveSkillConfig, resolveRuntimePlatform, } from "../agents/skills.js";
6
5
  import { ensureCredentialIndexSync } from "../credentials/store.js";
7
- import { NEXUS_ROOT } from "../utils.js";
6
+ import { MANAGED_SKILLS_DIR, NEXUS_ROOT } from "../utils.js";
8
7
  import { loadCapabilityRegistry } from "./registry.js";
9
8
  const STATUS_PRIORITY = [
10
9
  "active",
@@ -58,7 +57,7 @@ function setupLooksCredentialed(raw) {
58
57
  lowered.includes("key"));
59
58
  }
60
59
  function detectSkillUsageActive(skillName) {
61
- const usageLog = path.join(resolveStateDir(), "nexus", "skills", skillName, "usage.log");
60
+ const usageLog = path.join(MANAGED_SKILLS_DIR, skillName, "usage.log");
62
61
  try {
63
62
  const stat = fs.statSync(usageLog);
64
63
  return stat.isFile() && stat.size > 0;
@@ -67,7 +66,7 @@ function detectSkillUsageActive(skillName) {
67
66
  return false;
68
67
  }
69
68
  }
70
- function resolveSkillType(skillName, metadata) {
69
+ function resolveSkillType(_skillName, metadata) {
71
70
  if (metadata?.type)
72
71
  return metadata.type;
73
72
  const requires = metadata?.requires;
@@ -78,11 +77,16 @@ function resolveSkillType(skillName, metadata) {
78
77
  return "guide";
79
78
  }
80
79
  function resolveSkillProviderStatus(params) {
81
- const { skillName, providerId, config, credentialServices, metadata, setupHint } = params;
80
+ const { skillName, providerId, config, credentialServices, metadata, setupHint, } = params;
82
81
  const platform = resolveRuntimePlatform();
83
82
  const skillType = resolveSkillType(skillName, metadata);
84
83
  if (metadata?.os?.length && !metadata.os.includes(platform)) {
85
- return { id: providerId, kind: "skill", status: "unavailable", reason: "os" };
84
+ return {
85
+ id: providerId,
86
+ kind: "skill",
87
+ status: "unavailable",
88
+ reason: "os",
89
+ };
86
90
  }
87
91
  const skillKey = metadata?.skillKey ?? skillName;
88
92
  const skillConfig = resolveSkillConfig(config, skillKey);
@@ -108,15 +112,27 @@ function resolveSkillProviderStatus(params) {
108
112
  return true;
109
113
  });
110
114
  if (missingEnv.length > 0) {
111
- return { id: providerId, kind: "skill", status: "needs_setup", reason: "missing_env" };
115
+ return {
116
+ id: providerId,
117
+ kind: "skill",
118
+ status: "needs_setup",
119
+ reason: "missing_env",
120
+ };
112
121
  }
113
122
  const missingConfig = (requires?.config ?? []).filter((configPath) => !isConfigPathTruthy(config, configPath));
114
123
  if (missingConfig.length > 0) {
115
- return { id: providerId, kind: "skill", status: "needs_setup", reason: "missing_config" };
124
+ return {
125
+ id: providerId,
126
+ kind: "skill",
127
+ status: "needs_setup",
128
+ reason: "missing_config",
129
+ };
116
130
  }
117
131
  const credentialHints = requires?.credentials ?? [];
118
132
  const requiresCredential = setupLooksCredentialed(setupHint ?? "");
119
- if (skillType === "connector" || credentialHints.length > 0 || requiresCredential) {
133
+ if (skillType === "connector" ||
134
+ credentialHints.length > 0 ||
135
+ requiresCredential) {
120
136
  const aliases = new Set();
121
137
  for (const entry of [providerId, ...credentialHints]) {
122
138
  for (const alias of resolveCredentialAliases(entry))
@@ -124,7 +140,12 @@ function resolveSkillProviderStatus(params) {
124
140
  }
125
141
  const hasCred = Array.from(aliases).some((alias) => credentialServices.has(alias));
126
142
  if (!hasCred) {
127
- return { id: providerId, kind: "skill", status: "needs_setup", reason: "missing_credentials" };
143
+ return {
144
+ id: providerId,
145
+ kind: "skill",
146
+ status: "needs_setup",
147
+ reason: "missing_credentials",
148
+ };
128
149
  }
129
150
  }
130
151
  if (detectSkillUsageActive(skillName)) {
@@ -162,7 +183,9 @@ export function detectCapabilities(params) {
162
183
  const metadata = getSkillMetadata(entry);
163
184
  const key = normalizeProviderId(metadata?.skillKey ?? entry.skill.name);
164
185
  skillMap.set(key, { name: entry.skill.name, metadata });
165
- const provides = (metadata?.provides ?? []).map((cap) => cap.trim()).filter(Boolean);
186
+ const provides = (metadata?.provides ?? [])
187
+ .map((cap) => cap.trim())
188
+ .filter(Boolean);
166
189
  for (const capId of provides) {
167
190
  const normalizedCap = capId.trim();
168
191
  if (!normalizedCap)
@@ -176,7 +199,10 @@ export function detectCapabilities(params) {
176
199
  const capabilities = [];
177
200
  for (const capability of registry.all) {
178
201
  const extraProviders = providesMap.get(capability.id) ?? [];
179
- const providerList = Array.from(new Set([...capability.providers.map(normalizeProviderId), ...extraProviders]));
202
+ const providerList = Array.from(new Set([
203
+ ...capability.providers.map(normalizeProviderId),
204
+ ...extraProviders,
205
+ ]));
180
206
  const providerStatuses = [];
181
207
  for (const provider of providerList) {
182
208
  const normalized = normalizeProviderId(provider);
@@ -195,7 +221,11 @@ export function detectCapabilities(params) {
195
221
  providerStatuses.push(resolveCredentialProviderStatus(normalized, credentialServices));
196
222
  }
197
223
  else {
198
- providerStatuses.push({ id: normalized, kind: "unknown", status: "unavailable" });
224
+ providerStatuses.push({
225
+ id: normalized,
226
+ kind: "unknown",
227
+ status: "unavailable",
228
+ });
199
229
  }
200
230
  }
201
231
  const status = pickBestStatus(providerStatuses.map((p) => p.status));
@@ -206,7 +236,8 @@ export function detectCapabilities(params) {
206
236
  active: capabilities.filter((c) => c.status === "active").length,
207
237
  ready: capabilities.filter((c) => c.status === "ready").length,
208
238
  needs_setup: capabilities.filter((c) => c.status === "needs_setup").length,
209
- needs_install: capabilities.filter((c) => c.status === "needs_install").length,
239
+ needs_install: capabilities.filter((c) => c.status === "needs_install")
240
+ .length,
210
241
  unavailable: capabilities.filter((c) => c.status === "unavailable").length,
211
242
  broken: capabilities.filter((c) => c.status === "broken").length,
212
243
  };
@@ -70,7 +70,8 @@ export function loadCapabilityRegistry() {
70
70
  for (const line of lines) {
71
71
  const headingMatch = line.match(/^###\s+(.+)$/);
72
72
  if (headingMatch) {
73
- currentCategory = headingMatch[1]?.replace(/^[-–]\s*/, "").trim() || currentCategory;
73
+ currentCategory =
74
+ headingMatch[1]?.replace(/^[-–]\s*/, "").trim() || currentCategory;
74
75
  continue;
75
76
  }
76
77
  const tableMatch = line.match(/^\|\s*`([^`]+)`\s*\|\s*([^|]+)\|\s*([^|]+)\|\s*([^|]+)\|/);
@@ -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
  }
@@ -1,5 +1,5 @@
1
1
  import { cancel, confirm, isCancel } from "@clack/prompts";
2
- import { addCredential, flagCredential, getCredential, getCredentialPaths, listCredentials, removeCredential, scanCredentialEnv, } from "../commands/credential.js";
2
+ import { addCredential, flagCredential, getCredential, getCredentialPaths, getCredentialValue, importCliCredential, listCredentials, removeCredential, scanCredentialEnv, verifyCredentials, } from "../commands/credential.js";
3
3
  function parseFields(raw, fieldsArgs) {
4
4
  const out = {};
5
5
  if (raw) {
@@ -25,7 +25,9 @@ function parseFields(raw, fieldsArgs) {
25
25
  return out;
26
26
  }
27
27
  export function registerCredentialCli(program) {
28
- const credential = program.command("credential").description("Manage credentials");
28
+ const credential = program
29
+ .command("credential")
30
+ .description("Manage credentials");
29
31
  credential
30
32
  .command("list")
31
33
  .description("List credentials from the index")
@@ -54,29 +56,72 @@ export function registerCredentialCli(program) {
54
56
  const paths = getCredentialPaths();
55
57
  console.log(`\nIndex: ${paths.indexPath}`);
56
58
  });
59
+ credential
60
+ .command("verify <service>")
61
+ .description("Verify credential status for a service")
62
+ .option("--json", "Output as JSON")
63
+ .action(async (service, opts) => {
64
+ const result = await verifyCredentials({ service });
65
+ if (!result) {
66
+ console.error("Credential service not found.");
67
+ process.exit(1);
68
+ }
69
+ if (opts.json) {
70
+ console.log(JSON.stringify(result, null, 2));
71
+ return;
72
+ }
73
+ const iconFor = (status) => status === "ok" ? "✅" : status === "skipped" ? "⚠️" : "❌";
74
+ console.log(`${result.ok ? "✅" : "❌"} ${result.service}: ${result.accounts} account(s) checked`);
75
+ for (const entry of result.checked) {
76
+ const err = entry.error ? ` - ${entry.error}` : "";
77
+ console.log(` ${iconFor(entry.status)} ${entry.account} (${entry.authId})${err}`);
78
+ }
79
+ if (!result.ok) {
80
+ process.exit(1);
81
+ }
82
+ });
57
83
  credential
58
84
  .command("get")
59
- .description("Get a credential record")
85
+ .description("Get a credential value")
60
86
  .requiredOption("--service <id>", "Service id")
61
87
  .requiredOption("--account <id>", "Account id")
62
88
  .requiredOption("--auth <id>", "Auth id")
63
89
  .option("--json", "Output as JSON")
90
+ .option("--record", "Show credential record instead of value")
64
91
  .action(async (opts) => {
65
- const result = await getCredential({
92
+ const params = {
66
93
  service: opts.service,
67
94
  account: opts.account,
68
95
  authId: opts.auth,
69
- });
70
- if (!result) {
71
- console.error("Credential not found.");
96
+ };
97
+ if (opts.record) {
98
+ const result = await getCredential(params);
99
+ if (!result) {
100
+ console.error("Credential not found.");
101
+ process.exit(1);
102
+ }
103
+ if (opts.json) {
104
+ console.log(JSON.stringify(result, null, 2));
105
+ return;
106
+ }
107
+ console.log(`\n${result.filePath}`);
108
+ console.log(JSON.stringify(result.record, null, 2));
109
+ return;
110
+ }
111
+ const valueResult = await getCredentialValue(params);
112
+ if (!valueResult) {
113
+ console.error("Credential value not found.");
72
114
  process.exit(1);
73
115
  }
74
116
  if (opts.json) {
75
- console.log(JSON.stringify(result, null, 2));
117
+ console.log(JSON.stringify({
118
+ filePath: valueResult.filePath,
119
+ field: valueResult.field,
120
+ value: valueResult.value,
121
+ }, null, 2));
76
122
  return;
77
123
  }
78
- console.log(`\n${result.filePath}`);
79
- console.log(JSON.stringify(result.record, null, 2));
124
+ console.log(valueResult.value);
80
125
  });
81
126
  credential
82
127
  .command("add")
@@ -86,23 +131,55 @@ export function registerCredentialCli(program) {
86
131
  .requiredOption("--type <type>", "api_key | token | oauth | config")
87
132
  .option("--auth <id>", "Auth id (defaults to type)")
88
133
  .option("--owner <owner>", "shared | user | agent:<id>", "user")
89
- .option("--storage <provider>", "plaintext | keychain | 1password | external", "plaintext")
90
- .option("--value <value>", "Secret value for plaintext/keychain")
134
+ .option("--storage <provider>", "keychain | 1password | env | external")
135
+ .option("--value <value>", "Secret value for keychain")
91
136
  .option("--refresh-token <token>", "OAuth refresh token")
92
137
  .option("--expires-at <ts>", "Expiration (unix ms or ISO string)")
93
138
  .option("--vault <name>", "1Password vault")
94
139
  .option("--item <name>", "1Password item")
95
140
  .option("--fields <json>", "1Password field map as JSON")
96
141
  .option("--field <pair...>", "1Password field map: key=value")
97
- .option("--sync-command <cmd>", "External sync command")
142
+ .option("--env-var <name>", "Env var name for env storage")
143
+ .option("--command <cmd>", "External command to fetch secret")
144
+ .option("--sync-command <cmd>", "External sync command (deprecated)")
145
+ .option("--format <format>", "External command output format: raw|json")
146
+ .option("--json-path <path>", "JSON path for external output")
98
147
  .action(async (opts) => {
99
148
  const type = String(opts.type).trim();
100
149
  if (!["api_key", "token", "oauth", "config"].includes(type)) {
101
150
  console.error("Invalid --type");
102
151
  process.exit(1);
103
152
  }
104
- const storage = String(opts.storage).trim();
153
+ const storageRaw = opts.storage ? String(opts.storage).trim() : "";
154
+ const storage = storageRaw || (process.platform === "darwin" ? "keychain" : "");
155
+ if (!storage) {
156
+ console.error("Missing --storage. Use keychain (macOS), 1password, env, or external.");
157
+ process.exit(1);
158
+ }
159
+ if (storage === "keychain" && !opts.value) {
160
+ console.error("Missing --value for keychain storage.");
161
+ process.exit(1);
162
+ }
163
+ if (storage === "env" && !opts.envVar) {
164
+ console.error("Missing --env-var for env storage.");
165
+ process.exit(1);
166
+ }
167
+ if (storage === "external" && !opts.command && !opts.syncCommand) {
168
+ console.error("Missing --command for external storage.");
169
+ process.exit(1);
170
+ }
171
+ if (opts.format && !["raw", "json"].includes(String(opts.format))) {
172
+ console.error("Invalid --format. Use raw or json.");
173
+ process.exit(1);
174
+ }
175
+ if (!["keychain", "1password", "env", "external"].includes(storage)) {
176
+ console.error("Invalid --storage. Use keychain, 1password, env, external.");
177
+ process.exit(1);
178
+ }
105
179
  const fields = parseFields(opts.fields, opts.field);
180
+ const format = opts.format === "raw" || opts.format === "json"
181
+ ? opts.format
182
+ : undefined;
106
183
  const expiresAt = opts.expiresAt && /^\d+$/.test(String(opts.expiresAt).trim())
107
184
  ? Number.parseInt(String(opts.expiresAt).trim(), 10)
108
185
  : opts.expiresAt;
@@ -121,14 +198,59 @@ export function registerCredentialCli(program) {
121
198
  item: String(opts.item ?? ""),
122
199
  fields,
123
200
  }
124
- : storage === "external"
125
- ? { provider: "external", syncCommand: String(opts.syncCommand ?? "") }
126
- : { provider: "plaintext", value: opts.value },
201
+ : storage === "env"
202
+ ? { provider: "env", var: String(opts.envVar ?? "") }
203
+ : storage === "external"
204
+ ? {
205
+ provider: "external",
206
+ command: opts.command ? String(opts.command) : undefined,
207
+ syncCommand: opts.syncCommand
208
+ ? String(opts.syncCommand)
209
+ : undefined,
210
+ ...(format ? { format } : {}),
211
+ jsonPath: opts.jsonPath
212
+ ? String(opts.jsonPath)
213
+ : undefined,
214
+ }
215
+ : { provider: "env", var: String(opts.envVar ?? "") },
127
216
  refreshToken: opts.refreshToken,
128
217
  expiresAt,
129
218
  });
130
219
  console.log(`Added credential at ${record.filePath}`);
131
220
  });
221
+ credential
222
+ .command("import <source>")
223
+ .description("Import external CLI credentials")
224
+ .option("--account <id>", "Account id override")
225
+ .option("--owner <owner>", "shared | user | agent:<id>", "user")
226
+ .option("--force", "Overwrite existing record")
227
+ .option("--json", "Output as JSON")
228
+ .option("--no-keychain-prompt", "Skip keychain prompts")
229
+ .action(async (source, opts) => {
230
+ const normalized = String(source ?? "").trim();
231
+ if (!["claude-cli", "codex-cli"].includes(normalized)) {
232
+ console.error("Invalid source. Use claude-cli or codex-cli.");
233
+ process.exit(1);
234
+ }
235
+ try {
236
+ const result = await importCliCredential({
237
+ source: normalized,
238
+ account: opts.account,
239
+ owner: opts.owner,
240
+ force: Boolean(opts.force),
241
+ allowKeychainPrompt: opts.keychainPrompt !== false,
242
+ });
243
+ if (opts.json) {
244
+ console.log(JSON.stringify(result, null, 2));
245
+ return;
246
+ }
247
+ console.log(`Imported ${result.source} credentials into ${result.profileId} (${result.filePath})`);
248
+ }
249
+ catch (err) {
250
+ console.error(err instanceof Error ? err.message : String(err));
251
+ process.exit(1);
252
+ }
253
+ });
132
254
  credential
133
255
  .command("remove")
134
256
  .description("Remove a credential record")
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { CONFIG_PATH_NEXUS, loadConfig, resolveGatewayPort } from "../config/config.js";
2
+ import { CONFIG_PATH_NEXUS, loadConfig, resolveGatewayPort, } from "../config/config.js";
3
3
  import { GATEWAY_LAUNCH_AGENT_LABEL, GATEWAY_SYSTEMD_SERVICE_NAME, GATEWAY_WINDOWS_TASK_NAME, } from "../daemon/constants.js";
4
4
  import { resolveGatewayService } from "../daemon/service.js";
5
5
  import { callGateway, randomIdempotencyKey } from "../gateway/call.js";