nolo-cli 0.1.33 → 0.1.34

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 (56) hide show
  1. package/README.md +4 -4
  2. package/agent-runtime/agentRecordConfig.ts +8 -0
  3. package/agent-runtime/hostAdapter.ts +9 -1
  4. package/agent-runtime/index.ts +10 -0
  5. package/agent-runtime/localLoop.ts +49 -2
  6. package/agent-runtime/localToolPolicy.ts +70 -21
  7. package/agent-runtime/openAiCompatibleProvider.ts +7 -3
  8. package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
  9. package/agent-runtime/platformChatProvider.ts +48 -71
  10. package/agent-runtime/providerResolution.ts +244 -0
  11. package/agent-runtime/runtimeToolPolicy.ts +33 -0
  12. package/agent-runtime/shellCommandPolicy.ts +120 -0
  13. package/agentInternalCommandEntries.ts +2 -0
  14. package/agentListCommands.ts +2 -2
  15. package/agentPullCommand.ts +6 -11
  16. package/agentRecordCommands.ts +87 -2
  17. package/agentRecordHelpers.ts +87 -4
  18. package/agentRuntimeLocal.ts +3 -0
  19. package/ai/tools/toolNameAliases.ts +70 -0
  20. package/cli/agentInternalCommandEntries.ts +2 -0
  21. package/cli/agentListCommands.ts +2 -2
  22. package/cli/agentPullCommand.ts +6 -11
  23. package/cli/agentRecordCommands.ts +87 -2
  24. package/cli/agentRecordHelpers.ts +87 -4
  25. package/cli/agentRuntimeLocal.ts +3 -0
  26. package/cli/cliCommandDispatch.ts +2 -2
  27. package/cli/cliEnvHelpers.ts +80 -12
  28. package/cli/client/agentRun.ts +1 -0
  29. package/cli/client/localRuntimeAdapter.ts +44 -7
  30. package/cli/commandRegistry.ts +2 -2
  31. package/cli/machineCommands.ts +1 -1
  32. package/cli/machineWsRunDispatch.ts +29 -9
  33. package/cli/offlineMarxistsAgentCommand.ts +1 -1
  34. package/cli/runtimeDoctorCommands.ts +10 -4
  35. package/cli/tableCommands.ts +1 -1
  36. package/cli/taskRunCommand.ts +1 -1
  37. package/cliCommandDispatch.ts +2 -2
  38. package/cliEnvHelpers.ts +80 -12
  39. package/client/agentRun.ts +1 -0
  40. package/client/localProviderResolver.test.ts +26 -2
  41. package/client/localRuntimeAdapter.test.ts +287 -3
  42. package/client/localRuntimeAdapter.ts +44 -7
  43. package/commandRegistry.ts +2 -2
  44. package/database/server/authorityStoreTypes.ts +31 -0
  45. package/database/server/db.ts +15 -34
  46. package/database/server/legacyServerDb.ts +58 -0
  47. package/database/server/levelAuthorityStore.ts +75 -0
  48. package/database/server/memoryAuthorityStore.ts +133 -0
  49. package/database/server/serverStoreFactory.ts +118 -0
  50. package/machineCommands.ts +1 -1
  51. package/machineWsRunDispatch.ts +29 -9
  52. package/offlineMarxistsAgentCommand.ts +1 -1
  53. package/package.json +10 -2
  54. package/runtimeDoctorCommands.ts +10 -4
  55. package/tableCommands.ts +1 -1
  56. package/taskRunCommand.ts +1 -1
@@ -3,19 +3,17 @@ import { NOLO_CLUSTER_SERVERS } from "../database/config";
3
3
 
4
4
  export type EnvLike = Record<string, string | undefined>;
5
5
 
6
- export function resolveServerUrl(env: EnvLike) {
6
+ export function resolveServerUrlFromEnv(env: EnvLike) {
7
7
  return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
8
8
  }
9
9
 
10
- export function resolveServerCandidates(env: EnvLike, preferred?: string | null) {
11
- const values = [
12
- preferred?.trim(),
13
- env.NOLO_SERVER,
14
- env.BASE_URL,
15
- env.NOLO_SERVER_URL,
16
- DEFAULT_NOLO_SERVER_URL,
17
- ...NOLO_CLUSTER_SERVERS,
18
- ];
10
+ type ResolverInput = string[] | EnvLike;
11
+
12
+ function isCliArgs(value: ResolverInput): value is string[] {
13
+ return Array.isArray(value);
14
+ }
15
+
16
+ function buildServerCandidates(values: Array<string | null | undefined>) {
19
17
  return [...new Set(
20
18
  values
21
19
  .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
@@ -23,8 +21,78 @@ export function resolveServerCandidates(env: EnvLike, preferred?: string | null)
23
21
  )];
24
22
  }
25
23
 
26
- export function resolveAuthToken(env: EnvLike) {
27
- return env.AUTH_TOKEN || env.AUTH || "";
24
+ export function resolveServerUrl(env: EnvLike): string;
25
+ export function resolveServerUrl(args: string[], env: EnvLike): string;
26
+ export function resolveServerUrl(input: ResolverInput, env?: EnvLike) {
27
+ if (isCliArgs(input)) {
28
+ const explicit =
29
+ readOption(input, "--server-url") ||
30
+ readOption(input, "--server");
31
+ return explicit?.replace(/\/+$/, "") || resolveServerUrlFromEnv(env ?? {});
32
+ }
33
+ return resolveServerUrlFromEnv(input);
34
+ }
35
+
36
+ export function resolveServerCandidates(env: EnvLike, preferred?: string | null): string[];
37
+ export function resolveServerCandidates(
38
+ args: string[],
39
+ env: EnvLike,
40
+ preferred?: string | null,
41
+ ): string[];
42
+ export function resolveServerCandidates(
43
+ input: ResolverInput,
44
+ envOrPreferred?: EnvLike | string | null,
45
+ preferred?: string | null,
46
+ ) {
47
+ if (isCliArgs(input)) {
48
+ const env = (envOrPreferred ?? {}) as EnvLike;
49
+ const explicitServer = resolveServerUrl(input, env);
50
+ return buildServerCandidates([
51
+ explicitServer,
52
+ preferred?.trim(),
53
+ env.NOLO_SERVER,
54
+ env.BASE_URL,
55
+ env.NOLO_SERVER_URL,
56
+ DEFAULT_NOLO_SERVER_URL,
57
+ ...NOLO_CLUSTER_SERVERS,
58
+ ]);
59
+ }
60
+
61
+ const env = input;
62
+ return buildServerCandidates([
63
+ typeof envOrPreferred === "string" ? envOrPreferred.trim() : undefined,
64
+ env.NOLO_SERVER,
65
+ env.BASE_URL,
66
+ env.NOLO_SERVER_URL,
67
+ DEFAULT_NOLO_SERVER_URL,
68
+ ...NOLO_CLUSTER_SERVERS,
69
+ ]);
70
+ }
71
+
72
+ export function resolveAuthTokenFromEnv(env: EnvLike, extraEnvKeys: string[] = []) {
73
+ for (const key of ["AUTH_TOKEN", "AUTH", ...extraEnvKeys]) {
74
+ const value = env[key];
75
+ if (typeof value === "string" && value.trim()) return value;
76
+ }
77
+ return "";
78
+ }
79
+
80
+ export function resolveAuthToken(env: EnvLike, extraEnvKeys?: string[]): string;
81
+ export function resolveAuthToken(args: string[], env: EnvLike, extraEnvKeys?: string[]): string;
82
+ export function resolveAuthToken(
83
+ input: ResolverInput,
84
+ envOrExtraEnvKeys?: EnvLike | string[],
85
+ extraEnvKeys: string[] = [],
86
+ ) {
87
+ if (isCliArgs(input)) {
88
+ const env = (envOrExtraEnvKeys ?? {}) as EnvLike;
89
+ return (
90
+ readOption(input, "--token") ||
91
+ readOption(input, "--machine-key") ||
92
+ resolveAuthTokenFromEnv(env, extraEnvKeys)
93
+ );
94
+ }
95
+ return resolveAuthTokenFromEnv(input, (envOrExtraEnvKeys as string[] | undefined) ?? []);
28
96
  }
29
97
 
30
98
  export function parseUserIdFromAuthToken(token: string) {
@@ -778,6 +778,7 @@ async function runLocalAgentTurnForCli(
778
778
  background: options.background,
779
779
  noStream: options.noStream,
780
780
  maxToolRounds: options.maxToolRounds,
781
+ ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
781
782
  ...(traceLocalTools || collectToolEvidence
782
783
  ? {
783
784
  onToolEvent: (event) => {
@@ -16,6 +16,7 @@ import {
16
16
  parsePlatformChatCompletionData,
17
17
  parsePlatformChatCompletionResponse,
18
18
  resolvePlatformChatProviderConfig,
19
+ resolveRequestedRuntimeToolNames,
19
20
  shouldUsePlatformChatProvider,
20
21
  } from "../agentRuntimeLocal";
21
22
  import type { AgentRuntimeChatMessage } from "../../agent-runtime";
@@ -169,6 +170,12 @@ async function defaultLoopbackRequest(input: FetchInput, init?: FetchInit) {
169
170
  });
170
171
  });
171
172
  req.on("error", reject);
173
+ init?.signal?.addEventListener("abort", () => {
174
+ req.destroy(
175
+ init.signal?.reason instanceof Error ? init.signal.reason : new Error("request aborted")
176
+ );
177
+ reject(init.signal?.reason ?? new Error("request aborted"));
178
+ }, { once: true });
172
179
  if (body) req.write(body);
173
180
  req.end();
174
181
  });
@@ -191,6 +198,7 @@ async function fetchWithTransientRetry(
191
198
  }
192
199
  return await fetchImpl(input, init);
193
200
  } catch (error) {
201
+ if (init?.signal?.aborted) throw error;
194
202
  if (!isTransientFetchError(error)) throw error;
195
203
  lastError = error;
196
204
  if (attempt < TRANSIENT_FETCH_MAX_ATTEMPTS) {
@@ -201,6 +209,18 @@ async function fetchWithTransientRetry(
201
209
  throw lastError;
202
210
  }
203
211
 
212
+ function buildRequestTimeoutSignal(timeoutMs?: number) {
213
+ if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0) {
214
+ return undefined;
215
+ }
216
+ if (typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function") {
217
+ return AbortSignal.timeout(timeoutMs);
218
+ }
219
+ const controller = new AbortController();
220
+ setTimeout(() => controller.abort(new Error(`request timed out after ${timeoutMs}ms`)), timeoutMs);
221
+ return controller.signal;
222
+ }
223
+
204
224
  function buildOpenAiTools(args: { toolNames?: string[]; env: EnvLike }) {
205
225
  const toolset = buildLocalWorkspaceToolsetForEnv(args);
206
226
  return [
@@ -428,8 +448,11 @@ export function createCliLocalRuntimeAdapter(
428
448
  store: await resolveStore(deps),
429
449
  userId,
430
450
  });
451
+ const requestedToolNames = agentConfig
452
+ ? resolveRequestedRuntimeToolNames({ agentConfig })
453
+ : [];
431
454
  activeAgentToolNames = buildLocalPolicyToolNames({
432
- toolNames: agentConfig?.toolNames,
455
+ toolNames: requestedToolNames,
433
456
  env: deps.env,
434
457
  });
435
458
  await activateAgentWorkspaceSession({ agentRef, agentConfig });
@@ -455,9 +478,12 @@ export function createCliLocalRuntimeAdapter(
455
478
  });
456
479
  return {
457
480
  model: providerConfig.model,
458
- complete: async (messages) => {
481
+ complete: async (messages, options) => {
482
+ const requestedToolNames = resolveRequestedRuntimeToolNames({
483
+ agentConfig,
484
+ });
459
485
  const tools = buildOpenAiTools({
460
- toolNames: agentConfig.toolNames,
486
+ toolNames: requestedToolNames,
461
487
  env: deps.env,
462
488
  });
463
489
  const request = buildPlatformChatCompletionRequest({
@@ -465,7 +491,11 @@ export function createCliLocalRuntimeAdapter(
465
491
  messages,
466
492
  tools,
467
493
  });
468
- const res = await fetchWithTransientRetry(fetchImpl, request.url, request.init, {
494
+ const signal = buildRequestTimeoutSignal(options?.timeoutMs);
495
+ const res = await fetchWithTransientRetry(fetchImpl, request.url, {
496
+ ...request.init,
497
+ ...(signal ? { signal } : {}),
498
+ }, {
469
499
  sleep: deps.sleep,
470
500
  loopbackRequest,
471
501
  });
@@ -488,9 +518,12 @@ export function createCliLocalRuntimeAdapter(
488
518
  });
489
519
  return {
490
520
  model: providerConfig.model,
491
- complete: async (messages) => {
521
+ complete: async (messages, options) => {
522
+ const requestedToolNames = resolveRequestedRuntimeToolNames({
523
+ agentConfig,
524
+ });
492
525
  const tools = buildOpenAiTools({
493
- toolNames: agentConfig.toolNames,
526
+ toolNames: requestedToolNames,
494
527
  env: deps.env,
495
528
  });
496
529
  const request = buildOpenAiCompatibleChatCompletionRequest({
@@ -498,7 +531,11 @@ export function createCliLocalRuntimeAdapter(
498
531
  messages,
499
532
  tools,
500
533
  });
501
- const res = await fetchWithTransientRetry(fetchImpl, request.url, request.init, {
534
+ const signal = buildRequestTimeoutSignal(options?.timeoutMs);
535
+ const res = await fetchWithTransientRetry(fetchImpl, request.url, {
536
+ ...request.init,
537
+ ...(signal ? { signal } : {}),
538
+ }, {
502
539
  sleep: deps.sleep,
503
540
  loopbackRequest,
504
541
  });
@@ -49,8 +49,8 @@ export function renderHelpText() {
49
49
  " nolo connect",
50
50
  " nolo connect --watch",
51
51
  " nolo connect --ws",
52
- " nolo connect --ws --server-url https://api.nolo.chat --api-key sk_machine_xxx",
53
- " nolo connect --daemon --server-url https://api.nolo.chat --api-key sk_machine_xxx",
52
+ " nolo connect --ws --server-url https://api.nolo.chat --machine-key sk_machine_xxx",
53
+ " nolo connect --daemon --server-url https://api.nolo.chat --machine-key sk_machine_xxx",
54
54
  " nolo machine status",
55
55
  " nolo doctor",
56
56
  " nolo doctor runtime",
@@ -95,7 +95,7 @@ function resolveConnectServerUrl(args: string[], env: EnvLike) {
95
95
  }
96
96
 
97
97
  function resolveConnectAuthToken(args: string[], env: EnvLike) {
98
- return readOption(args, "--api-key") || readOption(args, "--token") || resolveAuthToken(env);
98
+ return readOption(args, "--machine-key") || readOption(args, "--token") || resolveAuthToken(env);
99
99
  }
100
100
 
101
101
  const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
@@ -148,6 +148,7 @@ function hasExplicitMachinePermissions(agentConfig: any) {
148
148
 
149
149
  function runtimeWorkspacePermissionPolicy(
150
150
  runtimePolicy: ConnectorRuntimePolicy | undefined,
151
+ runtimeEnv: EnvLike,
151
152
  cwd: string
152
153
  ): PermissionPolicy | null {
153
154
  if (!requestsTaskWorktree(runtimePolicy)) return null;
@@ -155,7 +156,10 @@ function runtimeWorkspacePermissionPolicy(
155
156
  mode: "full_access",
156
157
  allowFilesystemRead: true,
157
158
  allowFilesystemWrite: true,
158
- allowShell: runtimePolicy?.shell?.enabled !== false,
159
+ allowShell:
160
+ ["worktree", "dangerous", "1", "true"].includes(runtimeEnv.NOLO_LOCAL_SHELL_MODE || "") &&
161
+ Array.isArray(runtimePolicy?.runtimeTools) &&
162
+ runtimePolicy.runtimeTools.includes("execShell"),
159
163
  writableRoots: [cwd],
160
164
  };
161
165
  }
@@ -445,7 +449,7 @@ export async function handleConnectorRunMessage(
445
449
  artifactCwd = prepared.path;
446
450
  }
447
451
  const runtimePermissionPolicy = !hasExplicitMachinePermissions(agentConfig)
448
- ? runtimeWorkspacePermissionPolicy(runtimePolicy, cwd)
452
+ ? runtimeWorkspacePermissionPolicy(runtimePolicy, runtimeEnv, cwd)
449
453
  : null;
450
454
  const effectivePermissionPolicy = scopePermissionPolicyToRuntimeWorkspace(
451
455
  runtimePermissionPolicy ?? machinePermissionPolicy,
@@ -499,21 +503,37 @@ export async function handleConnectorRunMessage(
499
503
  const heartbeat = setInterval(() => {
500
504
  sendProgress({
501
505
  ...progressBase,
502
- message: "cli-running",
506
+ message: "cli-running",
503
507
  updatedAt: Date.now(),
504
508
  });
505
509
  }, 15_000);
510
+ const effectiveRuntimeEnv = withForwardedUserAuthToken(runtimeEnv, parsed);
506
511
  const result = await executeCli(provider, materializedPrompt.prompt, {
507
512
  model: agentConfig.model || undefined,
508
513
  timeout,
509
514
  cwd,
510
515
  yolo: true,
511
516
  env: {
512
- NOLO_SERVER: runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL || runtimeEnv.BASE_URL,
513
- NOLO_SERVER_URL: runtimeEnv.NOLO_SERVER_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL,
514
- BASE_URL: runtimeEnv.BASE_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL,
515
- AUTH_TOKEN: runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH || runtimeEnv.NOLO_MACHINE_API_KEY,
516
- NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
517
+ NOLO_SERVER:
518
+ effectiveRuntimeEnv.NOLO_SERVER ||
519
+ effectiveRuntimeEnv.NOLO_SERVER_URL ||
520
+ effectiveRuntimeEnv.BASE_URL,
521
+ NOLO_SERVER_URL:
522
+ effectiveRuntimeEnv.NOLO_SERVER_URL ||
523
+ effectiveRuntimeEnv.NOLO_SERVER ||
524
+ effectiveRuntimeEnv.BASE_URL,
525
+ BASE_URL:
526
+ effectiveRuntimeEnv.BASE_URL ||
527
+ effectiveRuntimeEnv.NOLO_SERVER ||
528
+ effectiveRuntimeEnv.NOLO_SERVER_URL,
529
+ AUTH_TOKEN:
530
+ effectiveRuntimeEnv.AUTH_TOKEN ||
531
+ effectiveRuntimeEnv.AUTH ||
532
+ effectiveRuntimeEnv.NOLO_MACHINE_API_KEY,
533
+ NOLO_MACHINE_API_KEY:
534
+ effectiveRuntimeEnv.NOLO_MACHINE_API_KEY ||
535
+ effectiveRuntimeEnv.AUTH_TOKEN ||
536
+ effectiveRuntimeEnv.AUTH,
517
537
  },
518
538
  }).finally(() => clearInterval(heartbeat));
519
539
  logConnectorRun("agent.run.cli.result", {
@@ -555,7 +575,7 @@ export async function handleConnectorRunMessage(
555
575
  baseSha = await readConnectorGitHead(cwd);
556
576
  const result = await runConnectorLocalRuntimeAgent({
557
577
  parsed,
558
- runtimeEnv,
578
+ runtimeEnv: withForwardedUserAuthToken(runtimeEnv, parsed),
559
579
  cwd,
560
580
  fetchImpl,
561
581
  onProgress: sendProgress,
@@ -28,7 +28,7 @@ type ParsedArgs = {
28
28
 
29
29
  const DEFAULT_USER_ID = "b2e06f801f";
30
30
  const DEFAULT_SPACE_ID = "01KKY77TT0DA9NY7TNW3R7255N";
31
- const DEFAULT_SOURCE_AGENT_ID = "01MIMO25MONTH00000001PGT4X";
31
+ const DEFAULT_SOURCE_AGENT_ID = "01MIMO25MONTH0000000NEW001";
32
32
  const DEFAULT_TARGET_AGENT_ID = "01OFFMARXBOOK000000010AHL1";
33
33
  const DEFAULT_AGENT_NAME = "离线马克思主义文库书籍转换助手";
34
34
  const TOOL_NAME = "convertMarxistsBookToOfflineHtml";
@@ -64,13 +64,13 @@ async function defaultLocalRuntimeProbe(env: EnvLike): Promise<LocalRuntimeProbe
64
64
  }
65
65
 
66
66
  export async function runDoctorRuntimeCommand(
67
- _args: string[],
67
+ args: string[],
68
68
  deps: AgentCommandDeps = {}
69
69
  ) {
70
70
  const env = deps.env ?? process.env;
71
71
  const output = deps.output ?? process.stdout;
72
- const authToken = resolveAuthToken(env);
73
- const serverUrl = resolveServerUrl(env);
72
+ const authToken = resolveAuthToken(args, env);
73
+ const serverUrl = resolveServerUrl(args, env);
74
74
  const localProbe = await (deps.localRuntimeProbe ?? defaultLocalRuntimeProbe)(env);
75
75
  const hasLocalAgentConfig = detectLocalAgentConfig(env) && localProbe.ok && localProbe.agentFound;
76
76
  const hasLocalProvider = detectLocalProvider(env);
@@ -88,7 +88,13 @@ export async function runDoctorRuntimeCommand(
88
88
  hasLocalPersistence: true,
89
89
  missingLocalCapabilities,
90
90
  requiresServer: false,
91
- serverFallbackAvailable: Boolean(authToken || env.NOLO_SERVER || env.BASE_URL),
91
+ serverFallbackAvailable: Boolean(
92
+ authToken ||
93
+ args.includes("--server") ||
94
+ args.includes("--server-url") ||
95
+ env.NOLO_SERVER ||
96
+ env.BASE_URL
97
+ ),
92
98
  });
93
99
 
94
100
  output.write(`Runtime: ${decision.mode}\n`);
@@ -80,7 +80,7 @@ function resolveServerUrl(args: string[], env: EnvLike): string {
80
80
 
81
81
  function resolveAuthToken(args: string[], env: EnvLike): string {
82
82
  return (
83
- readOption(args, "--api-key") ||
83
+ readOption(args, "--machine-key") ||
84
84
  readOption(args, "--token") ||
85
85
  env.AUTH_TOKEN ||
86
86
  env.AUTH ||
@@ -82,7 +82,7 @@ function resolveServerUrl(args: string[], env: EnvLike) {
82
82
 
83
83
  function resolveAuthToken(args: string[], env: EnvLike) {
84
84
  return (
85
- readOption(args, "--api-key") ||
85
+ readOption(args, "--machine-key") ||
86
86
  readOption(args, "--token") ||
87
87
  env.NOLO_TASK_RUN_TOKEN ||
88
88
  env.NOLO_MACHINE_API_KEY ||
@@ -31,7 +31,7 @@ function readOption(args: string[], flag: string) {
31
31
 
32
32
  function buildDaemonEnv(args: string[], runtimeEnv: NodeJS.ProcessEnv) {
33
33
  const serverUrl = readOption(args, "--server-url") || readOption(args, "--server");
34
- const apiKey = readOption(args, "--api-key") || readOption(args, "--token");
34
+ const apiKey = readOption(args, "--machine-key") || readOption(args, "--token");
35
35
  return {
36
36
  ...runtimeEnv,
37
37
  ...(serverUrl ? { NOLO_SERVER: serverUrl } : {}),
@@ -40,7 +40,7 @@ function buildDaemonEnv(args: string[], runtimeEnv: NodeJS.ProcessEnv) {
40
40
  }
41
41
 
42
42
  export function looksLikeDaemonShortcut(args: string[]) {
43
- return args.includes("--server-url") || args.includes("--api-key");
43
+ return args.includes("--server-url") || args.includes("--machine-key");
44
44
  }
45
45
 
46
46
  export function createCliRuntimeContext(args: {
package/cliEnvHelpers.ts CHANGED
@@ -3,19 +3,17 @@ import { NOLO_CLUSTER_SERVERS } from "./database/config";
3
3
 
4
4
  export type EnvLike = Record<string, string | undefined>;
5
5
 
6
- export function resolveServerUrl(env: EnvLike) {
6
+ export function resolveServerUrlFromEnv(env: EnvLike) {
7
7
  return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
8
8
  }
9
9
 
10
- export function resolveServerCandidates(env: EnvLike, preferred?: string | null) {
11
- const values = [
12
- preferred?.trim(),
13
- env.NOLO_SERVER,
14
- env.BASE_URL,
15
- env.NOLO_SERVER_URL,
16
- DEFAULT_NOLO_SERVER_URL,
17
- ...NOLO_CLUSTER_SERVERS,
18
- ];
10
+ type ResolverInput = string[] | EnvLike;
11
+
12
+ function isCliArgs(value: ResolverInput): value is string[] {
13
+ return Array.isArray(value);
14
+ }
15
+
16
+ function buildServerCandidates(values: Array<string | null | undefined>) {
19
17
  return [...new Set(
20
18
  values
21
19
  .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
@@ -23,8 +21,78 @@ export function resolveServerCandidates(env: EnvLike, preferred?: string | null)
23
21
  )];
24
22
  }
25
23
 
26
- export function resolveAuthToken(env: EnvLike) {
27
- return env.AUTH_TOKEN || env.AUTH || "";
24
+ export function resolveServerUrl(env: EnvLike): string;
25
+ export function resolveServerUrl(args: string[], env: EnvLike): string;
26
+ export function resolveServerUrl(input: ResolverInput, env?: EnvLike) {
27
+ if (isCliArgs(input)) {
28
+ const explicit =
29
+ readOption(input, "--server-url") ||
30
+ readOption(input, "--server");
31
+ return explicit?.replace(/\/+$/, "") || resolveServerUrlFromEnv(env ?? {});
32
+ }
33
+ return resolveServerUrlFromEnv(input);
34
+ }
35
+
36
+ export function resolveServerCandidates(env: EnvLike, preferred?: string | null): string[];
37
+ export function resolveServerCandidates(
38
+ args: string[],
39
+ env: EnvLike,
40
+ preferred?: string | null,
41
+ ): string[];
42
+ export function resolveServerCandidates(
43
+ input: ResolverInput,
44
+ envOrPreferred?: EnvLike | string | null,
45
+ preferred?: string | null,
46
+ ) {
47
+ if (isCliArgs(input)) {
48
+ const env = (envOrPreferred ?? {}) as EnvLike;
49
+ const explicitServer = resolveServerUrl(input, env);
50
+ return buildServerCandidates([
51
+ explicitServer,
52
+ preferred?.trim(),
53
+ env.NOLO_SERVER,
54
+ env.BASE_URL,
55
+ env.NOLO_SERVER_URL,
56
+ DEFAULT_NOLO_SERVER_URL,
57
+ ...NOLO_CLUSTER_SERVERS,
58
+ ]);
59
+ }
60
+
61
+ const env = input;
62
+ return buildServerCandidates([
63
+ typeof envOrPreferred === "string" ? envOrPreferred.trim() : undefined,
64
+ env.NOLO_SERVER,
65
+ env.BASE_URL,
66
+ env.NOLO_SERVER_URL,
67
+ DEFAULT_NOLO_SERVER_URL,
68
+ ...NOLO_CLUSTER_SERVERS,
69
+ ]);
70
+ }
71
+
72
+ export function resolveAuthTokenFromEnv(env: EnvLike, extraEnvKeys: string[] = []) {
73
+ for (const key of ["AUTH_TOKEN", "AUTH", ...extraEnvKeys]) {
74
+ const value = env[key];
75
+ if (typeof value === "string" && value.trim()) return value;
76
+ }
77
+ return "";
78
+ }
79
+
80
+ export function resolveAuthToken(env: EnvLike, extraEnvKeys?: string[]): string;
81
+ export function resolveAuthToken(args: string[], env: EnvLike, extraEnvKeys?: string[]): string;
82
+ export function resolveAuthToken(
83
+ input: ResolverInput,
84
+ envOrExtraEnvKeys?: EnvLike | string[],
85
+ extraEnvKeys: string[] = [],
86
+ ) {
87
+ if (isCliArgs(input)) {
88
+ const env = (envOrExtraEnvKeys ?? {}) as EnvLike;
89
+ return (
90
+ readOption(input, "--token") ||
91
+ readOption(input, "--machine-key") ||
92
+ resolveAuthTokenFromEnv(env, extraEnvKeys)
93
+ );
94
+ }
95
+ return resolveAuthTokenFromEnv(input, (envOrExtraEnvKeys as string[] | undefined) ?? []);
28
96
  }
29
97
 
30
98
  export function parseUserIdFromAuthToken(token: string) {
@@ -778,6 +778,7 @@ async function runLocalAgentTurnForCli(
778
778
  background: options.background,
779
779
  noStream: options.noStream,
780
780
  maxToolRounds: options.maxToolRounds,
781
+ ...(typeof options.timeoutMs === "number" ? { timeoutMs: options.timeoutMs } : {}),
781
782
  ...(traceLocalTools || collectToolEvidence
782
783
  ? {
783
784
  onToolEvent: (event) => {
@@ -11,15 +11,18 @@ describe("CLI local provider resolver", () => {
11
11
  provider: "custom",
12
12
  apiSource: "custom",
13
13
  customProviderUrl: "https://provider.example/v1/chat/completions",
14
+ apiKey: "sk-agent-custom",
15
+ apiKeyHeader: "api-key",
14
16
  },
15
17
  env: {
16
- OPENAI_API_KEY: "sk-env",
18
+ OPENAI_API_KEY: "sk-env-should-not-win",
17
19
  NOLO_LOCAL_OPENAI_BASE_URL: "http://127.0.0.1:11434/v1",
18
20
  },
19
21
  })).toEqual({
20
22
  model: "qwen-coder",
21
23
  endpoint: "https://provider.example/v1/chat/completions",
22
- apiKey: "sk-env",
24
+ apiKey: "sk-agent-custom",
25
+ apiKeyHeader: "api-key",
23
26
  provider: "custom",
24
27
  requestOptions: {},
25
28
  });
@@ -53,6 +56,27 @@ describe("CLI local provider resolver", () => {
53
56
  });
54
57
  });
55
58
 
59
+ test("defaults Xiaomi custom endpoints to api-key auth header", () => {
60
+ expect(resolveCliOpenAiProviderConfig({
61
+ agentConfig: {
62
+ key: "agent-user-1-mimo",
63
+ provider: "custom",
64
+ apiSource: "custom",
65
+ model: "mimo-v2.5-pro",
66
+ customProviderUrl: "https://token-plan-cn.xiaomimimo.com/v1",
67
+ apiKey: "mimo-monthly-key",
68
+ },
69
+ env: {},
70
+ })).toEqual({
71
+ model: "mimo-v2.5-pro",
72
+ endpoint: "https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
73
+ apiKey: "mimo-monthly-key",
74
+ apiKeyHeader: "api-key",
75
+ provider: "custom",
76
+ requestOptions: {},
77
+ });
78
+ });
79
+
56
80
  test("carries explicit agent inference options for request body construction", () => {
57
81
  expect(resolveCliOpenAiProviderConfig({
58
82
  agentConfig: {