@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
@@ -0,0 +1,41 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveStateDir } from "../config/paths.js";
4
+ function listAgentIds(stateDir) {
5
+ const agentsDir = path.join(stateDir, "agents");
6
+ try {
7
+ const entries = fs.readdirSync(agentsDir, { withFileTypes: true });
8
+ return entries
9
+ .filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
10
+ .map((entry) => entry.name)
11
+ .sort();
12
+ }
13
+ catch {
14
+ return [];
15
+ }
16
+ }
17
+ export function resolveAgentId(env = process.env) {
18
+ const override = env.NEXUS_AGENT_ID?.trim();
19
+ const stateDir = resolveStateDir(env);
20
+ const available = listAgentIds(stateDir);
21
+ if (override) {
22
+ return {
23
+ ok: true,
24
+ agentId: override,
25
+ source: "env",
26
+ available,
27
+ };
28
+ }
29
+ if (available.length === 1) {
30
+ return {
31
+ ok: true,
32
+ agentId: available[0],
33
+ source: "auto",
34
+ available,
35
+ };
36
+ }
37
+ if (available.length > 1) {
38
+ return { ok: false, reason: "multiple", available };
39
+ }
40
+ return { ok: true, agentId: "default", source: "default", available };
41
+ }
@@ -1,8 +1,8 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import lockfile from "proper-lockfile";
4
+ import { buildCredentialIndex, ensureCredentialIndexSync, listCredentialEntriesSync, readCredentialRecordSync, resolveDefaultEnvVar, resolveOAuthBundle, resolveCredentialIndexPath, resolveCredentialValue, writeCredentialIndexSync, writeCredentialRecordSync, } from "../credentials/store.js";
4
5
  import { createSubsystemLogger } from "../logging.js";
5
- import { buildCredentialIndex, ensureCredentialIndexSync, listCredentialEntriesSync, readCredentialRecordSync, resolveCredentialIndexPath, resolveCredentialValue, writeCredentialIndexSync, writeCredentialRecordSync, } from "../credentials/store.js";
6
6
  import { refreshChutesTokens } from "./chutes-oauth.js";
7
7
  import { normalizeProviderId } from "./model-selection.js";
8
8
  export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
@@ -22,17 +22,24 @@ function parseProfileId(profileId) {
22
22
  const parts = profileId.split(":");
23
23
  if (parts.length === 1)
24
24
  return { provider: profileId, account: "default" };
25
- return { provider: parts[0] ?? profileId, account: parts.slice(1).join(":") || "default" };
25
+ return {
26
+ provider: parts[0] ?? profileId,
27
+ account: parts.slice(1).join(":") || "default",
28
+ };
26
29
  }
27
30
  function isEmailLike(value) {
28
31
  const trimmed = value.trim();
29
- return Boolean(trimmed && trimmed.includes("@") && trimmed.includes("."));
32
+ return Boolean(trimmed?.includes("@") && trimmed.includes("."));
30
33
  }
31
34
  function pickPreferredEntry(entries) {
32
35
  if (entries.length === 0)
33
36
  return null;
34
37
  const byType = (type) => entries.find((entry) => entry.record.type === type);
35
- return byType("api_key") ?? byType("token") ?? byType("oauth") ?? entries[0] ?? null;
38
+ return (byType("api_key") ??
39
+ byType("token") ??
40
+ byType("oauth") ??
41
+ entries[0] ??
42
+ null);
36
43
  }
37
44
  function resolveCredentialEmail(entry) {
38
45
  const metaEmail = entry.record.metadata?.email;
@@ -113,7 +120,8 @@ export function loadAuthProfileStore() {
113
120
  rebuilt.order = existing.order;
114
121
  rebuilt.lastGood = existing.lastGood;
115
122
  rebuilt.usageStats = existing.usageStats;
116
- const servicesChanged = JSON.stringify(rebuilt.services) !== JSON.stringify(existing.services ?? {});
123
+ const servicesChanged = JSON.stringify(rebuilt.services) !==
124
+ JSON.stringify(existing.services ?? {});
117
125
  if (servicesChanged) {
118
126
  writeIndex(rebuilt);
119
127
  }
@@ -166,6 +174,31 @@ export async function setAuthProfileOrder(params) {
166
174
  function resolveAuthIdForProvider(entry) {
167
175
  return entry.authId;
168
176
  }
177
+ function buildAuthPayload(credential) {
178
+ if (credential.type === "api_key") {
179
+ return { value: credential.key ?? "" };
180
+ }
181
+ if (credential.type === "token") {
182
+ const value = credential.token ?? "";
183
+ if (!credential.expires)
184
+ return { value };
185
+ return {
186
+ value: JSON.stringify({
187
+ token: value,
188
+ expiresAt: credential.expires,
189
+ }),
190
+ format: "json",
191
+ };
192
+ }
193
+ return {
194
+ value: JSON.stringify({
195
+ accessToken: credential.access,
196
+ refreshToken: credential.refresh,
197
+ expiresAt: credential.expires,
198
+ }),
199
+ format: "json",
200
+ };
201
+ }
169
202
  export function upsertAuthProfile(params) {
170
203
  const { provider, account } = parseProfileId(params.profileId);
171
204
  const authId = resolveAuthIdForProvider({
@@ -176,33 +209,43 @@ export function upsertAuthProfile(params) {
176
209
  record: {
177
210
  owner: "user",
178
211
  type: params.credential.type,
179
- storage: { provider: "plaintext" },
212
+ storage: { provider: "env", var: "NEXUS_DUMMY" },
180
213
  },
181
214
  });
182
215
  const record = {
183
216
  owner: "user",
184
217
  type: params.credential.type,
185
218
  configuredAt: new Date().toISOString(),
186
- storage: { provider: "plaintext" },
219
+ storage: { provider: "env", var: "NEXUS_DUMMY" },
187
220
  metadata: {
188
221
  addedBy: "nexus upsertAuthProfile",
189
222
  },
223
+ ...(params.credential.type !== "api_key" && params.credential.expires
224
+ ? { expiresAt: params.credential.expires }
225
+ : {}),
190
226
  };
191
227
  if (params.credential.type === "api_key") {
192
228
  record.key = params.credential.key;
193
229
  }
194
230
  else if (params.credential.type === "token") {
195
231
  record.token = params.credential.token;
196
- record.expiresAt = params.credential.expires;
197
232
  }
198
233
  else {
199
234
  record.accessToken = params.credential.access;
200
235
  record.refreshToken = params.credential.refresh;
201
- record.expiresAt = params.credential.expires;
202
236
  }
203
- const secretValue = record.key ?? record.token ?? record.accessToken;
237
+ const payload = buildAuthPayload(params.credential);
238
+ const envVar = resolveDefaultEnvVar({
239
+ service: provider,
240
+ type: params.credential.type,
241
+ });
242
+ record.storage = {
243
+ provider: "env",
244
+ var: envVar,
245
+ ...(payload.format ? { format: payload.format } : {}),
246
+ };
204
247
  const allowKeychain = process.env.NEXUS_KEYCHAIN_ENABLED === "1";
205
- if (secretValue && allowKeychain && process.platform === "darwin") {
248
+ if (payload.value && allowKeychain && process.platform === "darwin") {
206
249
  const keychainService = `nexus.${provider}`;
207
250
  const keychainAccount = account;
208
251
  try {
@@ -214,18 +257,26 @@ export function upsertAuthProfile(params) {
214
257
  "-a",
215
258
  keychainAccount,
216
259
  "-w",
217
- secretValue,
260
+ payload.value,
218
261
  ]);
219
- record.storage = { provider: "keychain", service: keychainService, account: keychainAccount };
220
- record.key = undefined;
221
- record.token = undefined;
222
- record.accessToken = undefined;
223
- record.refreshToken = undefined;
262
+ record.storage = {
263
+ provider: "keychain",
264
+ service: keychainService,
265
+ account: keychainAccount,
266
+ ...(payload.format ? { format: payload.format } : {}),
267
+ };
224
268
  }
225
269
  catch (err) {
226
- log.warn("keychain write failed; storing plaintext credential", { err: String(err) });
270
+ process.env[envVar] = payload.value;
271
+ log.warn("keychain write failed; using env credential fallback", {
272
+ err: String(err),
273
+ envVar,
274
+ });
227
275
  }
228
276
  }
277
+ else if (payload.value) {
278
+ process.env[envVar] = payload.value;
279
+ }
229
280
  writeCredentialRecordSync(provider, account, authId, record);
230
281
  const index = readIndex();
231
282
  index.lastUpdated = new Date().toISOString();
@@ -302,7 +353,10 @@ export async function clearAuthProfileCooldown(params) {
302
353
  export async function markAuthProfileGood(params) {
303
354
  const providerKey = normalizeProviderId(params.provider);
304
355
  const updated = await updateIndexWithLock((index) => {
305
- index.lastGood = { ...(index.lastGood ?? {}), [providerKey]: params.profileId };
356
+ index.lastGood = {
357
+ ...index.lastGood,
358
+ [providerKey]: params.profileId,
359
+ };
306
360
  return true;
307
361
  });
308
362
  if (updated) {
@@ -337,17 +391,52 @@ export async function resolveApiKeyForProfile(params) {
337
391
  const providerKey = normalizeProviderId(cred.provider);
338
392
  if (providerKey === "chutes" && record.type === "oauth") {
339
393
  try {
394
+ const bundle = await resolveOAuthBundle(record);
395
+ const accessToken = bundle.accessToken ?? record.accessToken;
396
+ const refreshToken = bundle.refreshToken ?? record.refreshToken;
397
+ const oauthExpires = bundle.expiresAt !== undefined ? bundle.expiresAt : record.expiresAt;
398
+ const oauthExpiresAt = typeof oauthExpires === "number"
399
+ ? oauthExpires
400
+ : Number.parseInt(String(oauthExpires), 10);
401
+ if (!accessToken || !refreshToken) {
402
+ throw new Error("Missing OAuth refresh token for chutes.");
403
+ }
340
404
  const refreshed = await refreshChutesTokens({
341
405
  credential: {
342
- access: record.accessToken,
343
- refresh: record.refreshToken,
344
- expires: expiresAt,
406
+ access: accessToken,
407
+ refresh: refreshToken,
408
+ expires: oauthExpiresAt,
345
409
  email: cred.email,
346
- clientId: typeof record.metadata?.clientId === "string" ? record.metadata.clientId : undefined,
410
+ clientId: typeof record.metadata?.clientId === "string"
411
+ ? record.metadata.clientId
412
+ : undefined,
347
413
  },
348
414
  });
349
- record.accessToken = refreshed.access;
350
- record.refreshToken = refreshed.refresh;
415
+ const refreshedPayload = JSON.stringify({
416
+ accessToken: refreshed.access,
417
+ refreshToken: refreshed.refresh,
418
+ expiresAt: refreshed.expires,
419
+ });
420
+ if (record.storage.provider === "keychain") {
421
+ execFileSync("security", [
422
+ "add-generic-password",
423
+ "-U",
424
+ "-s",
425
+ record.storage.service,
426
+ "-a",
427
+ record.storage.account,
428
+ "-w",
429
+ refreshedPayload,
430
+ ]);
431
+ record.storage = { ...record.storage, format: "json" };
432
+ }
433
+ else if (record.storage.provider === "env") {
434
+ process.env[record.storage.var] = refreshedPayload;
435
+ record.storage = { ...record.storage, format: "json" };
436
+ }
437
+ else {
438
+ throw new Error("Cannot persist refreshed token without keychain/env.");
439
+ }
351
440
  record.expiresAt = refreshed.expires;
352
441
  record.lastVerified = new Date().toISOString();
353
442
  if (typeof refreshed.clientId === "string") {
@@ -0,0 +1,79 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { resolveBootstrapPath, resolveStateDir } from "../config/paths.js";
4
+ import { resolveAgentId } from "./agent-id.js";
5
+ function readField(pathname, label) {
6
+ try {
7
+ const raw = fs.readFileSync(pathname, "utf-8");
8
+ const regex = new RegExp(`^[-*]?\\s*${label}\\s*:\\s*(.+)$`, "im");
9
+ const match = raw.match(regex);
10
+ return match?.[1]?.trim();
11
+ }
12
+ catch {
13
+ return undefined;
14
+ }
15
+ }
16
+ function hasPopulatedField(pathname) {
17
+ try {
18
+ const raw = fs.readFileSync(pathname, "utf-8");
19
+ const lines = raw.split(/\r?\n/);
20
+ return lines.some((line) => {
21
+ const match = line.match(/^[-*]?\s*[^:]+:\s*(.+)$/);
22
+ if (!match)
23
+ return false;
24
+ return match[1].trim().length > 0;
25
+ });
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function resolveAgentFromState(resolution, stateDir) {
32
+ if (!resolution.ok) {
33
+ return {
34
+ ok: false,
35
+ reason: "multiple_agents",
36
+ agentOptions: resolution.available,
37
+ };
38
+ }
39
+ const agentId = resolution.agentId;
40
+ const agentIdentityDir = path.join(stateDir, "agents", agentId, "identity");
41
+ const userIdentityDir = path.join(stateDir, "user", "identity");
42
+ const agentIdentityPath = path.join(agentIdentityDir, "IDENTITY.md");
43
+ const agentSoulPath = path.join(agentIdentityDir, "SOUL.md");
44
+ const agentMemoryPath = path.join(agentIdentityDir, "MEMORY.md");
45
+ const userProfilePath = path.join(userIdentityDir, "PROFILE.md");
46
+ const bootstrapPath = resolveBootstrapPath(undefined, stateDir);
47
+ const agentIdentityExists = fs.existsSync(agentIdentityPath);
48
+ const agentSoulExists = fs.existsSync(agentSoulPath);
49
+ const agentMemoryExists = fs.existsSync(agentMemoryPath);
50
+ const userProfileExists = fs.existsSync(userProfilePath);
51
+ const hasIdentity = hasPopulatedField(agentIdentityPath) && hasPopulatedField(userProfilePath);
52
+ const agentName = readField(agentIdentityPath, "Name");
53
+ const userName = readField(userProfilePath, "Name");
54
+ return {
55
+ ok: true,
56
+ snapshot: {
57
+ agentId,
58
+ agentIdSource: resolution.source,
59
+ agentOptions: resolution.available,
60
+ agentName,
61
+ userName,
62
+ agentIdentityPath,
63
+ agentSoulPath,
64
+ agentMemoryPath,
65
+ userProfilePath,
66
+ bootstrapPath,
67
+ agentIdentityExists,
68
+ agentSoulExists,
69
+ agentMemoryExists,
70
+ userProfileExists,
71
+ hasIdentity,
72
+ },
73
+ };
74
+ }
75
+ export function resolveIdentitySnapshot(env = process.env) {
76
+ const stateDir = resolveStateDir(env);
77
+ const resolution = resolveAgentId(env);
78
+ return resolveAgentFromState(resolution, stateDir);
79
+ }
@@ -97,6 +97,7 @@ export function resolveEnvApiKey(provider) {
97
97
  openrouter: "OPENROUTER_API_KEY",
98
98
  zai: "ZAI_API_KEY",
99
99
  mistral: "MISTRAL_API_KEY",
100
+ minimax: "MINIMAX_API_KEY",
100
101
  };
101
102
  const envVar = envMap[provider];
102
103
  if (!envVar)
@@ -1,5 +1,5 @@
1
1
  import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
2
- import { buildModelAliasIndex, modelKey, parseModelRef, resolveModelRefFromString, } from "./model-selection.js";
2
+ import { buildModelAliasIndex, modelKey, parseModelRef, resolveConfiguredModelRef, resolveModelRefFromString, } from "./model-selection.js";
3
3
  function isAbortError(err) {
4
4
  if (!err || typeof err !== "object")
5
5
  return false;
@@ -78,8 +78,13 @@ function resolveImageFallbackCandidates(params) {
78
78
  return candidates;
79
79
  }
80
80
  function resolveFallbackCandidates(params) {
81
- const provider = params.provider.trim() || DEFAULT_PROVIDER;
82
- const model = params.model.trim() || DEFAULT_MODEL;
81
+ const fallbackDefault = resolveConfiguredModelRef({
82
+ cfg: params.cfg ?? {},
83
+ defaultProvider: DEFAULT_PROVIDER,
84
+ defaultModel: DEFAULT_MODEL,
85
+ });
86
+ const provider = params.provider?.trim() || fallbackDefault.provider;
87
+ const model = params.model?.trim() || fallbackDefault.model;
83
88
  const aliasIndex = buildModelAliasIndex({
84
89
  cfg: params.cfg ?? {},
85
90
  defaultProvider: DEFAULT_PROVIDER,
@@ -99,12 +104,13 @@ function resolveFallbackCandidates(params) {
99
104
  candidates.push(candidate);
100
105
  };
101
106
  addCandidate({ provider, model }, false);
102
- const modelFallbacks = (() => {
103
- const model = params.cfg?.agent?.model;
104
- if (model && typeof model === "object")
105
- return model.fallbacks ?? [];
106
- return [];
107
- })();
107
+ const modelFallbacks = params.fallbacksOverride ??
108
+ (() => {
109
+ const model = params.cfg?.agent?.model;
110
+ if (model && typeof model === "object")
111
+ return model.fallbacks ?? [];
112
+ return [];
113
+ })();
108
114
  for (const raw of modelFallbacks) {
109
115
  const resolved = resolveModelRefFromString({
110
116
  raw: String(raw ?? ""),
@@ -132,7 +132,7 @@ export function buildAllowedModelSet(params) {
132
132
  }
133
133
  }
134
134
  const allowedCatalog = params.catalog.filter((entry) => allowedKeys.has(modelKey(entry.provider, entry.id)));
135
- if (allowedCatalog.length === 0) {
135
+ if (allowedCatalog.length === 0 && allowedKeys.size === 0) {
136
136
  return {
137
137
  allowAny: true,
138
138
  allowedCatalog: params.catalog,
@@ -83,7 +83,7 @@ function resolveMinimaxApiKeyFromStore(store) {
83
83
  }
84
84
  return undefined;
85
85
  }
86
- function resolveImplicitProviders(params) {
86
+ function _resolveImplicitProviders(params) {
87
87
  const providers = {};
88
88
  const minimaxEnv = resolveEnvApiKey("minimax");
89
89
  const authStore = ensureAuthProfileStore(params.agentDir);
@@ -93,7 +93,7 @@ function resolveImplicitProviders(params) {
93
93
  }
94
94
  return providers;
95
95
  }
96
- async function maybeBuildCopilotProvider(params) {
96
+ async function _maybeBuildCopilotProvider(params) {
97
97
  const env = params.env ?? process.env;
98
98
  const authStore = ensureAuthProfileStore(params.agentDir);
99
99
  const profileIds = listProfilesForProvider(authStore, "github-copilot");
@@ -106,7 +106,10 @@ async function maybeBuildCopilotProvider(params) {
106
106
  if (!selectedGithubToken && hasProfile) {
107
107
  const profileId = profileIds[0];
108
108
  if (profileId) {
109
- const resolved = await resolveApiKeyForProfile({ store: authStore, profileId });
109
+ const resolved = await resolveApiKeyForProfile({
110
+ store: authStore,
111
+ profileId,
112
+ });
110
113
  if (resolved?.apiKey)
111
114
  selectedGithubToken = resolved.apiKey;
112
115
  }
@@ -134,17 +137,20 @@ async function maybeBuildCopilotProvider(params) {
134
137
  }
135
138
  export async function ensureNexusModelsJson(config, agentDirOverride) {
136
139
  const cfg = config ?? loadConfig();
137
- const providers = cfg.models?.providers;
138
- if (!providers || Object.keys(providers).length === 0) {
139
- const agentDir = agentDirOverride?.trim()
140
- ? agentDirOverride.trim()
141
- : resolveNexusAgentDir();
142
- return { agentDir, wrote: false };
143
- }
144
- const mode = cfg.models?.mode ?? DEFAULT_MODE;
145
140
  const agentDir = agentDirOverride?.trim()
146
141
  ? agentDirOverride.trim()
147
142
  : resolveNexusAgentDir();
143
+ const explicitProviders = cfg.models?.providers ?? {};
144
+ const implicitProviders = _resolveImplicitProviders({ agentDir });
145
+ const copilotProvider = await _maybeBuildCopilotProvider({ agentDir });
146
+ if (copilotProvider && !explicitProviders["github-copilot"]) {
147
+ implicitProviders["github-copilot"] = copilotProvider;
148
+ }
149
+ const providers = { ...implicitProviders, ...explicitProviders };
150
+ if (Object.keys(providers).length === 0) {
151
+ return { agentDir, wrote: false };
152
+ }
153
+ const mode = cfg.models?.mode ?? DEFAULT_MODE;
148
154
  const targetPath = path.join(agentDir, "models.json");
149
155
  let mergedProviders = providers;
150
156
  let existingRaw = "";
@@ -1,11 +1,13 @@
1
1
  import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
+ import { streamSimple } from "@mariozechner/pi-ai";
3
4
  import { buildSystemPrompt, createAgentSession, discoverAuthStorage, discoverModels, SessionManager, SettingsManager, } from "@mariozechner/pi-coding-agent";
4
5
  import { formatToolAggregate } from "../auto-reply/tool-meta.js";
5
6
  import { getMachineDisplayName } from "../infra/machine-name.js";
6
7
  import { createSubsystemLogger } from "../logging.js";
7
8
  import { splitMediaFromOutput } from "../media/parse.js";
8
9
  import { enqueueCommandInLane, } from "../process/command-queue.js";
10
+ import { resolveTelegramReactionLevel } from "../telegram/reaction-level.js";
9
11
  import { resolveUserPath } from "../utils.js";
10
12
  import { resolveNexusAgentDir } from "./agent-paths.js";
11
13
  import { markAuthProfileCooldown, markAuthProfileGood, markAuthProfileUsed, } from "./auth-profiles.js";
@@ -18,7 +20,6 @@ import { extractAssistantText } from "./pi-embedded-utils.js";
18
20
  import { toToolDefinitions } from "./pi-tool-definition-adapter.js";
19
21
  import { createNexusCodingTools } from "./pi-tools.js";
20
22
  import { resolveSandboxContext } from "./sandbox.js";
21
- import { resolveTelegramReactionLevel } from "../telegram/reaction-level.js";
22
23
  import { applySkillEnvOverrides, applySkillEnvOverridesFromSnapshot, buildWorkspaceSkillSnapshot, loadWorkspaceSkillEntries, } from "./skills.js";
23
24
  import { buildAgentSystemPromptAppend } from "./system-prompt.js";
24
25
  import { normalizeUsage } from "./usage.js";
@@ -46,7 +47,7 @@ const GOOGLE_SCHEMA_UNSUPPORTED_KEYWORDS = new Set([
46
47
  "minProperties",
47
48
  "maxProperties",
48
49
  ]);
49
- function isAntigravityClaudeModel(provider, modelId) {
50
+ function _isAntigravityClaudeModel(provider, modelId) {
50
51
  if (provider !== "google-antigravity")
51
52
  return false;
52
53
  return modelId.trim().toLowerCase().includes("claude");
@@ -69,8 +70,9 @@ function findUnsupportedSchemaKeywords(schema, path) {
69
70
  }
70
71
  return violations;
71
72
  }
72
- function logToolSchemasForGoogle(params) {
73
- if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") {
73
+ function _logToolSchemasForGoogle(params) {
74
+ if (params.provider !== "google-antigravity" &&
75
+ params.provider !== "google-gemini-cli") {
74
76
  return;
75
77
  }
76
78
  const toolNames = params.tools.map((tool, index) => `${index}:${tool.name}`);
@@ -91,6 +93,89 @@ function logToolSchemasForGoogle(params) {
91
93
  }
92
94
  }
93
95
  }
96
+ /**
97
+ * Resolve provider-specific extra params from model config.
98
+ *
99
+ * Example config:
100
+ * agent.models["anthropic/claude-sonnet-4-5"].params.temperature = 0.7
101
+ * agent.models["openai/gpt-4.1-mini"].params.maxTokens = 8192
102
+ */
103
+ export function resolveExtraParams(params) {
104
+ const modelKey = `${params.provider}/${params.modelId}`;
105
+ const modelConfig = params.cfg?.agent?.models?.[modelKey];
106
+ let extraParams = modelConfig?.params ? { ...modelConfig.params } : undefined;
107
+ // Auto-enable thinking for ZAI GLM-4.x models when not explicitly configured.
108
+ // Skip if user explicitly disabled thinking via --thinking off.
109
+ if (params.provider === "zai" && params.thinkLevel !== "off") {
110
+ const modelIdLower = params.modelId.toLowerCase();
111
+ const isGlm4 = modelIdLower.includes("glm-4");
112
+ if (isGlm4) {
113
+ const hasThinkingConfig = extraParams?.thinking !== undefined;
114
+ if (!hasThinkingConfig) {
115
+ // GLM-4.7 supports preserved thinking (reasoning kept across turns).
116
+ // GLM-4.5/4.6 use interleaved thinking (reasoning cleared each turn).
117
+ const isGlm47 = modelIdLower.includes("glm-4.7");
118
+ const clearThinking = !isGlm47;
119
+ extraParams = {
120
+ ...extraParams,
121
+ thinking: {
122
+ type: "enabled",
123
+ clear_thinking: clearThinking,
124
+ },
125
+ };
126
+ log.debug(`auto-enabled thinking for ${modelKey}: type=enabled, clear_thinking=${clearThinking}`);
127
+ }
128
+ }
129
+ }
130
+ return extraParams;
131
+ }
132
+ /**
133
+ * Create a wrapped streamFn that injects extra params (like temperature) from config.
134
+ *
135
+ * This wraps the default streamSimple with config-driven params for each model.
136
+ */
137
+ function createStreamFnWithExtraParams(extraParams) {
138
+ if (!extraParams || Object.keys(extraParams).length === 0) {
139
+ return undefined;
140
+ }
141
+ const streamParams = {};
142
+ if (typeof extraParams.temperature === "number") {
143
+ streamParams.temperature = extraParams.temperature;
144
+ }
145
+ if (typeof extraParams.maxTokens === "number") {
146
+ streamParams.maxTokens = extraParams.maxTokens;
147
+ }
148
+ if (Object.keys(streamParams).length === 0) {
149
+ return undefined;
150
+ }
151
+ log.debug(`creating streamFn wrapper with params: ${JSON.stringify(streamParams)}`);
152
+ const wrappedStreamFn = (model, context, options) => {
153
+ const mergedOptions = {
154
+ ...streamParams,
155
+ ...options,
156
+ };
157
+ return streamSimple(model, context, mergedOptions);
158
+ };
159
+ return wrappedStreamFn;
160
+ }
161
+ /**
162
+ * Apply extra params (like temperature) to an agent's streamFn.
163
+ *
164
+ * Call this after createAgentSession to wire up config-driven model params.
165
+ */
166
+ function applyExtraParamsToAgent(agent, cfg, provider, modelId, thinkLevel) {
167
+ const extraParams = resolveExtraParams({
168
+ cfg,
169
+ provider,
170
+ modelId,
171
+ thinkLevel,
172
+ });
173
+ const wrappedStreamFn = createStreamFnWithExtraParams(extraParams);
174
+ if (wrappedStreamFn) {
175
+ log.debug(`applying extraParams to agent streamFn for ${provider}/${modelId}`);
176
+ agent.streamFn = wrappedStreamFn;
177
+ }
178
+ }
94
179
  /**
95
180
  * Limits conversation history to the last N user turns (and their associated
96
181
  * assistant responses). This reduces token usage for long-running DM sessions.
@@ -136,7 +221,9 @@ export function getDmHistoryLimitFromSessionKey(sessionKey, config, messageChann
136
221
  const raw = sessionKey?.trim() ?? "";
137
222
  if (!raw)
138
223
  return undefined;
139
- if (raw.startsWith("group:") || raw.includes(":group:") || raw.includes(":channel:")) {
224
+ if (raw.startsWith("group:") ||
225
+ raw.includes(":group:") ||
226
+ raw.includes(":channel:")) {
140
227
  return undefined;
141
228
  }
142
229
  const parts = raw.split(":").filter(Boolean);
@@ -176,7 +263,7 @@ export function getDmHistoryLimitFromSessionKey(sessionKey, config, messageChann
176
263
  return undefined;
177
264
  }
178
265
  }
179
- function resolveReactionGuidance(params) {
266
+ function _resolveReactionGuidance(params) {
180
267
  if (!params.config)
181
268
  return undefined;
182
269
  const rawChannel = params.messageChannel ?? params.messageProvider ?? "";
@@ -488,6 +575,7 @@ export async function compactEmbeddedPiSession(params) {
488
575
  skills: promptSkills,
489
576
  contextFiles,
490
577
  });
578
+ applyExtraParamsToAgent(session.agent, params.config, provider, modelId, params.thinkLevel);
491
579
  try {
492
580
  const prior = await sanitizeSessionMessagesImages(session.messages, "session:history");
493
581
  if (prior.length > 0) {
@@ -705,6 +793,7 @@ export async function runEmbeddedPiAgent(params) {
705
793
  skills: promptSkills,
706
794
  contextFiles,
707
795
  });
796
+ applyExtraParamsToAgent(session.agent, params.config, provider, modelId, thinkLevel);
708
797
  const prior = await sanitizeSessionMessagesImages(session.messages, "session:history");
709
798
  if (prior.length > 0) {
710
799
  session.agent.replaceMessages(prior);
@@ -837,7 +926,7 @@ export async function runEmbeddedPiAgent(params) {
837
926
  if (shouldRotate) {
838
927
  // Mark current profile for cooldown before rotating
839
928
  if (lastProfileId) {
840
- markAuthProfileCooldown({
929
+ void markAuthProfileCooldown({
841
930
  store: authStore,
842
931
  profileId: lastProfileId,
843
932
  });
@@ -906,13 +995,16 @@ export async function runEmbeddedPiAgent(params) {
906
995
  .filter((p) => p.text || p.mediaUrl || (p.mediaUrls && p.mediaUrls.length > 0));
907
996
  log.debug(`embedded run done: runId=${params.runId} sessionId=${params.sessionId} durationMs=${Date.now() - started} aborted=${aborted}`);
908
997
  if (lastProfileId) {
909
- markAuthProfileGood({
998
+ void markAuthProfileGood({
910
999
  store: authStore,
911
1000
  provider,
912
1001
  profileId: lastProfileId,
913
1002
  });
914
1003
  // Track usage for round-robin rotation
915
- markAuthProfileUsed({ store: authStore, profileId: lastProfileId });
1004
+ void markAuthProfileUsed({
1005
+ store: authStore,
1006
+ profileId: lastProfileId,
1007
+ });
916
1008
  }
917
1009
  return {
918
1010
  payloads: payloads.length ? payloads : undefined,