nolo-cli 0.1.33 → 0.1.35

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 (84) hide show
  1. package/README.md +15 -5
  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 +71 -22
  7. package/agent-runtime/localWorkspaceTools.ts +5 -2
  8. package/agent-runtime/openAiCompatibleProvider.ts +7 -3
  9. package/agent-runtime/openAiCompatibleProviderConfig.ts +15 -26
  10. package/agent-runtime/platformChatProvider.ts +48 -71
  11. package/agent-runtime/providerResolution.ts +242 -0
  12. package/agent-runtime/runtimeToolPolicy.ts +33 -0
  13. package/agent-runtime/shellCommandPolicy.ts +120 -0
  14. package/agentCommandSupport.ts +4 -0
  15. package/agentInternalCommandEntries.ts +2 -0
  16. package/agentListCommands.ts +74 -12
  17. package/agentListHelpers.ts +56 -1
  18. package/agentPullCommand.ts +6 -11
  19. package/agentRecordCommands.ts +87 -2
  20. package/agentRecordHelpers.ts +132 -9
  21. package/agentRuntimeLocal.ts +3 -0
  22. package/ai/tools/toolNameAliases.ts +70 -0
  23. package/cli/agentCommandSupport.ts +4 -0
  24. package/cli/agentInternalCommandEntries.ts +2 -0
  25. package/cli/agentListCommands.ts +74 -12
  26. package/cli/agentListHelpers.ts +56 -1
  27. package/cli/agentPullCommand.ts +6 -11
  28. package/cli/agentRecordCommands.ts +87 -2
  29. package/cli/agentRecordHelpers.ts +132 -9
  30. package/cli/agentRuntimeLocal.ts +3 -0
  31. package/cli/cliCommandDispatch.ts +2 -2
  32. package/cli/cliEnvHelpers.ts +80 -12
  33. package/cli/cliSpaceHelpers.ts +39 -0
  34. package/cli/client/agentRun.ts +1 -0
  35. package/cli/client/localRuntimeAdapter.ts +44 -7
  36. package/cli/commandRegistry.ts +2 -2
  37. package/cli/connectorWebSocketTarget.ts +10 -3
  38. package/cli/dialogCommands.ts +446 -0
  39. package/cli/dialogInternalCommandEntries.ts +13 -0
  40. package/cli/globalRecordOperations.ts +258 -0
  41. package/cli/internalCommandEntries.ts +2 -0
  42. package/cli/localRuntimeAuthority.ts +182 -0
  43. package/cli/localRuntimeDb.ts +2 -4
  44. package/cli/machineCommands.ts +1 -1
  45. package/cli/machineWsRunDispatch.ts +29 -12
  46. package/cli/machineWsSession.ts +2 -0
  47. package/cli/offlineMarxistsAgentCommand.ts +1 -1
  48. package/cli/runtimeDoctorCommands.ts +65 -9
  49. package/cli/tableCommands.ts +1 -1
  50. package/cli/taskRunCommand.ts +1 -1
  51. package/cliCommandDispatch.ts +2 -2
  52. package/cliEnvHelpers.ts +80 -12
  53. package/cliSpaceHelpers.ts +39 -0
  54. package/client/agentRun.ts +1 -0
  55. package/client/localProviderResolver.test.ts +26 -2
  56. package/client/localRuntimeAdapter.test.ts +287 -3
  57. package/client/localRuntimeAdapter.ts +44 -7
  58. package/commandRegistry.ts +2 -2
  59. package/connectorWebSocketTarget.ts +10 -3
  60. package/database/server/authorityStoreTypes.ts +31 -0
  61. package/database/server/cliAuthorityBrokerClient.ts +227 -0
  62. package/database/server/cliAuthorityBrokerServer.ts +283 -0
  63. package/database/server/cliAuthorityBrokerTypes.ts +43 -0
  64. package/database/server/cliAuthorityStoreDriver.ts +98 -0
  65. package/database/server/legacyServerDb.ts +58 -0
  66. package/database/server/levelAuthorityStore.ts +95 -0
  67. package/database/tombstones.ts +52 -0
  68. package/database/userDataMerge.ts +95 -0
  69. package/dialogCommands.ts +446 -0
  70. package/dialogInternalCommandEntries.ts +13 -0
  71. package/globalRecordOperations.ts +258 -0
  72. package/internalCommandEntries.ts +2 -0
  73. package/localRuntimeAuthority.ts +182 -0
  74. package/localRuntimeDb.ts +2 -4
  75. package/machineCommands.ts +1 -1
  76. package/machineWsRunDispatch.ts +29 -12
  77. package/machineWsSession.ts +2 -0
  78. package/offlineMarxistsAgentCommand.ts +1 -1
  79. package/package.json +24 -4
  80. package/runtimeDoctorCommands.ts +65 -9
  81. package/tableCommands.ts +1 -1
  82. package/taskRunCommand.ts +1 -1
  83. package/database/server/db.ts +0 -118
  84. package/database/server/ensureDbOpen.ts +0 -12
@@ -0,0 +1,242 @@
1
+ import type { AgentRuntimeAgentConfig } from "./hostAdapter";
2
+ import { pickAgentRuntimeInferenceOptions } from "./agentConfigOptions";
3
+
4
+ type EnvLike = Record<string, string | undefined>;
5
+
6
+ const PROVIDER_ENDPOINTS: Record<string, string> = {
7
+ deepinfra: "https://api.deepinfra.com/v1/openai/chat/completions",
8
+ deepseek: "https://api.deepseek.com/chat/completions",
9
+ fireworks: "https://api.fireworks.ai/inference/v1/chat/completions",
10
+ google: "https://generativelanguage.googleapis.com/v1beta/openai/chat/completions",
11
+ mistral: "https://api.mistral.ai/v1/chat/completions",
12
+ mimo: "https://token-plan-cn.xiaomimimo.com/v1/chat/completions",
13
+ openai: "https://api.openai.com/v1/chat/completions",
14
+ openrouter: "https://openrouter.ai/api/v1/chat/completions",
15
+ };
16
+
17
+ export type AgentProviderMode = "cli" | "platform" | "custom";
18
+ export type ProviderExecutionTransport = "direct" | "proxy";
19
+
20
+ type ProviderExecutionPlanBase = {
21
+ mode: AgentProviderMode;
22
+ transport: ProviderExecutionTransport;
23
+ model: string;
24
+ provider: string;
25
+ requestOptions: Record<string, number | string>;
26
+ };
27
+
28
+ export type CliProviderExecutionPlan = ProviderExecutionPlanBase & {
29
+ mode: "cli";
30
+ transport: "direct";
31
+ cliProvider: string;
32
+ };
33
+
34
+ export type DirectProviderExecutionPlan = ProviderExecutionPlanBase & {
35
+ mode: "platform" | "custom";
36
+ transport: "direct";
37
+ endpoint: string;
38
+ apiKey: string;
39
+ apiKeyHeader?: string;
40
+ };
41
+
42
+ export type ProxyProviderExecutionPlan = ProviderExecutionPlanBase & {
43
+ mode: "platform" | "custom";
44
+ transport: "proxy";
45
+ endpoint: string;
46
+ serverUrl: string;
47
+ authToken: string;
48
+ agentKey: string;
49
+ apiSource?: string;
50
+ apiKey?: string;
51
+ apiKeyHeader?: string;
52
+ };
53
+
54
+ export type ProviderExecutionPlan =
55
+ | CliProviderExecutionPlan
56
+ | DirectProviderExecutionPlan
57
+ | ProxyProviderExecutionPlan;
58
+
59
+ export function trimTrailingSlash(value: string) {
60
+ return value.trim().replace(/\/+$/, "");
61
+ }
62
+
63
+ export function resolveChatCompletionsEndpoint(value: string) {
64
+ const trimmed = trimTrailingSlash(value);
65
+ return trimmed.endsWith("/chat/completions")
66
+ ? trimmed
67
+ : `${trimmed}/chat/completions`;
68
+ }
69
+
70
+ export function resolveOpenAiCompatibleBaseUrl(env: EnvLike) {
71
+ return env.NOLO_LOCAL_OPENAI_BASE_URL || env.OPENAI_BASE_URL || "https://api.openai.com/v1";
72
+ }
73
+
74
+ export function resolveOpenAiCompatibleApiKey(env: EnvLike) {
75
+ return env.OPENAI_API_KEY || env.NOLO_LOCAL_OPENAI_API_KEY || "";
76
+ }
77
+
78
+ export function resolvePlatformServerUrl(env: EnvLike) {
79
+ return trimTrailingSlash(env.NOLO_SERVER || env.BASE_URL || "https://nolo.chat");
80
+ }
81
+
82
+ export function resolvePlatformAuthToken(env: EnvLike) {
83
+ return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
84
+ }
85
+
86
+ export function canUsePlatformChatProvider(env: EnvLike) {
87
+ return Boolean(resolvePlatformAuthToken(env));
88
+ }
89
+
90
+ export function hasDirectOpenAiCompatibleProvider(env: EnvLike) {
91
+ return Boolean(
92
+ env.NOLO_LOCAL_OPENAI_API_KEY ||
93
+ env.OPENAI_API_KEY ||
94
+ env.NOLO_LOCAL_OPENAI_BASE_URL ||
95
+ env.OPENAI_BASE_URL ||
96
+ env.OLLAMA_BASE_URL
97
+ );
98
+ }
99
+
100
+ function resolvePlatformProviderEndpoint(agentConfig: AgentRuntimeAgentConfig) {
101
+ const customProviderUrl = agentConfig.customProviderUrl?.trim();
102
+ if (customProviderUrl) return resolveChatCompletionsEndpoint(customProviderUrl);
103
+
104
+ const provider = agentConfig.provider?.trim().toLowerCase();
105
+ if (!provider) {
106
+ throw new Error("Platform chat provider requires agentConfig.provider.");
107
+ }
108
+ const endpoint = PROVIDER_ENDPOINTS[provider];
109
+ if (!endpoint) {
110
+ throw new Error(`Platform chat provider does not support provider "${provider}".`);
111
+ }
112
+ return endpoint;
113
+ }
114
+
115
+ export function resolveAgentProviderMode(agentConfig: AgentRuntimeAgentConfig): AgentProviderMode {
116
+ if (agentConfig.apiSource === "cli" || agentConfig.provider === "cli" || agentConfig.cliProvider) {
117
+ return "cli";
118
+ }
119
+ if (agentConfig.apiSource === "custom" || agentConfig.customProviderUrl) {
120
+ return "custom";
121
+ }
122
+ return "platform";
123
+ }
124
+
125
+ export function shouldUsePlatformProxyTransport(
126
+ env: EnvLike,
127
+ agentConfig?: AgentRuntimeAgentConfig
128
+ ) {
129
+ if (agentConfig?.apiSource === "platform" || agentConfig?.useServerProxy === true) return true;
130
+ if (env.NOLO_LOCAL_LLM === "platform") return true;
131
+ if (env.NOLO_LOCAL_LLM === "direct") return false;
132
+ return !hasDirectOpenAiCompatibleProvider(env) && canUsePlatformChatProvider(env);
133
+ }
134
+
135
+ export function resolveProviderAuthHeaderName(args: {
136
+ endpoint: string;
137
+ apiKeyHeader?: string;
138
+ }) {
139
+ const explicitHeader = args.apiKeyHeader?.trim();
140
+ if (explicitHeader) return explicitHeader;
141
+ if (/xiaomimimo\.com/i.test(args.endpoint)) return "api-key";
142
+ return "Authorization";
143
+ }
144
+
145
+ export function buildProviderAuthHeaders(args: {
146
+ endpoint: string;
147
+ apiKey: string;
148
+ apiKeyHeader?: string;
149
+ }) {
150
+ if (!args.apiKey) return {};
151
+ const headerName = resolveProviderAuthHeaderName(args);
152
+ return headerName.toLowerCase() === "authorization"
153
+ ? { Authorization: `Bearer ${args.apiKey}` }
154
+ : { [headerName]: args.apiKey };
155
+ }
156
+
157
+ export function buildProviderExecutionPlan(args: {
158
+ agentConfig: AgentRuntimeAgentConfig;
159
+ env: EnvLike;
160
+ runtimeKind: "local" | "desktop" | "server";
161
+ }): ProviderExecutionPlan {
162
+ const { agentConfig, env } = args;
163
+ const mode = resolveAgentProviderMode(agentConfig);
164
+ const model = agentConfig.model || "gpt-4.1-mini";
165
+ const requestOptions = pickAgentRuntimeInferenceOptions(agentConfig);
166
+
167
+ if (mode === "cli") {
168
+ return {
169
+ mode,
170
+ transport: "direct",
171
+ cliProvider: agentConfig.cliProvider || agentConfig.provider || "codex",
172
+ model,
173
+ provider: agentConfig.provider || "cli",
174
+ requestOptions,
175
+ };
176
+ }
177
+
178
+ if (mode === "custom") {
179
+ const endpoint = resolveChatCompletionsEndpoint(agentConfig.customProviderUrl || resolveOpenAiCompatibleBaseUrl(env));
180
+ const apiKey = agentConfig.apiKey?.trim() || agentConfig.apiKeyFromAgentKey?.trim() || "";
181
+ const apiKeyHeader = resolveProviderAuthHeaderName({
182
+ endpoint,
183
+ apiKeyHeader: agentConfig.apiKeyHeader,
184
+ });
185
+ if (agentConfig.useServerProxy) {
186
+ return {
187
+ mode,
188
+ transport: "proxy",
189
+ model,
190
+ provider: agentConfig.provider || "custom",
191
+ endpoint,
192
+ requestOptions,
193
+ serverUrl: resolvePlatformServerUrl(env),
194
+ authToken: resolvePlatformAuthToken(env),
195
+ agentKey: agentConfig.key,
196
+ ...(agentConfig.apiSource ? { apiSource: agentConfig.apiSource } : {}),
197
+ ...(apiKey ? { apiKey } : {}),
198
+ ...(apiKeyHeader ? { apiKeyHeader } : {}),
199
+ };
200
+ }
201
+ return {
202
+ mode,
203
+ transport: "direct",
204
+ model,
205
+ provider: agentConfig.provider || "custom",
206
+ endpoint,
207
+ requestOptions,
208
+ apiKey,
209
+ ...(apiKeyHeader ? { apiKeyHeader } : {}),
210
+ };
211
+ }
212
+
213
+ const provider = agentConfig.provider || agentConfig.apiSource || "openai";
214
+ const endpoint = shouldUsePlatformProxyTransport(env, agentConfig)
215
+ ? resolvePlatformProviderEndpoint(agentConfig)
216
+ : resolveChatCompletionsEndpoint(resolveOpenAiCompatibleBaseUrl(env));
217
+ if (shouldUsePlatformProxyTransport(env, agentConfig)) {
218
+ return {
219
+ mode,
220
+ transport: "proxy",
221
+ model,
222
+ provider,
223
+ endpoint,
224
+ requestOptions,
225
+ serverUrl: resolvePlatformServerUrl(env),
226
+ authToken: resolvePlatformAuthToken(env),
227
+ agentKey: agentConfig.key,
228
+ ...(agentConfig.apiSource ? { apiSource: agentConfig.apiSource } : {}),
229
+ ...(agentConfig.apiKey?.trim() ? { apiKey: agentConfig.apiKey.trim() } : {}),
230
+ ...(agentConfig.apiKeyHeader?.trim() ? { apiKeyHeader: agentConfig.apiKeyHeader.trim() } : {}),
231
+ };
232
+ }
233
+ return {
234
+ mode,
235
+ transport: "direct",
236
+ model,
237
+ provider: agentConfig.provider || "openai-compatible",
238
+ endpoint,
239
+ requestOptions,
240
+ apiKey: resolveOpenAiCompatibleApiKey(env),
241
+ };
242
+ }
@@ -46,6 +46,16 @@ export function normalizeAgentRuntimeToolPolicy(
46
46
  };
47
47
  }
48
48
 
49
+ function runtimeBindingRecord(
50
+ runtimeBinding: unknown,
51
+ ): Record<string, unknown> | undefined {
52
+ return isRecord(runtimeBinding) ? runtimeBinding : undefined;
53
+ }
54
+
55
+ function runtimeToolNamesFromPolicy(policy: AgentRuntimeToolPolicy | undefined) {
56
+ return stringArray(policy?.runtimeTools);
57
+ }
58
+
49
59
  export function mergeAgentRuntimeToolPolicies(
50
60
  base?: AgentRuntimeToolPolicy,
51
61
  override?: AgentRuntimeToolPolicy,
@@ -77,6 +87,29 @@ export function mergeAgentRuntimeToolPolicies(
77
87
  };
78
88
  }
79
89
 
90
+ export function resolveRequestedRuntimeToolNames(args: {
91
+ agentConfig: Pick<
92
+ AgentRuntimeAgentConfig,
93
+ "toolNames" | "runtimeToolPolicy" | "runtimeBinding"
94
+ >;
95
+ }) {
96
+ const runtimeBinding = runtimeBindingRecord(args.agentConfig.runtimeBinding);
97
+ const bindingPolicy = normalizeAgentRuntimeToolPolicy(
98
+ runtimeBinding?.runtimeToolPolicy,
99
+ );
100
+ const bindingSnapshotPolicy = normalizeAgentRuntimeToolPolicy(
101
+ runtimeBinding?.runtimeToolPolicySnapshot,
102
+ );
103
+ return unique([
104
+ ...stringArray(args.agentConfig.toolNames),
105
+ ...runtimeToolNamesFromPolicy(
106
+ normalizeAgentRuntimeToolPolicy(args.agentConfig.runtimeToolPolicy),
107
+ ),
108
+ ...runtimeToolNamesFromPolicy(bindingPolicy),
109
+ ...runtimeToolNamesFromPolicy(bindingSnapshotPolicy),
110
+ ]);
111
+ }
112
+
80
113
  function policyRequestsHostedWorkspace(policy: AgentRuntimeToolPolicy | undefined) {
81
114
  if (!policy) return false;
82
115
  const workspaceMode = policy.workspace?.mode;
@@ -0,0 +1,120 @@
1
+ export type ShellCommandPolicyVerdict = "allowed" | "forbidden";
2
+
3
+ export type ShellCommandPolicyResult =
4
+ | {
5
+ verdict: "allowed";
6
+ }
7
+ | {
8
+ verdict: "forbidden";
9
+ code: "destructive_action_requires_confirmation";
10
+ reason: string;
11
+ policy: {
12
+ capability: "destructive_action";
13
+ target: "shell_command";
14
+ detail: string;
15
+ };
16
+ };
17
+
18
+ const DESTRUCTIVE_NEGATION_PATTERNS = [
19
+ "别删",
20
+ "别删除",
21
+ "不要删",
22
+ "不要删除",
23
+ "别乱删",
24
+ "别动",
25
+ "不要动",
26
+ "不要清理",
27
+ "不要清空",
28
+ "不要重置",
29
+ "don't delete",
30
+ "do not delete",
31
+ "don't remove",
32
+ "do not remove",
33
+ "without deleting",
34
+ ] as const;
35
+
36
+ const EXPLICIT_DESTRUCTIVE_REQUEST_PATTERNS = [
37
+ "删除",
38
+ "删掉",
39
+ "移除",
40
+ "清理",
41
+ "清空",
42
+ "重置",
43
+ "drop",
44
+ "delete",
45
+ "remove",
46
+ "erase",
47
+ "clean up",
48
+ "reset",
49
+ ] as const;
50
+
51
+ const SHELL_DESTRUCTIVE_PATTERNS = [
52
+ /\brm\s+(-[^\n\r]*\s+)?/i,
53
+ /\bremove-item\b/i,
54
+ /\bdel\s+\/?[a-z]*\s+/i,
55
+ /\berase\s+/i,
56
+ /\brmdir\s+/i,
57
+ /\brd\s+\/s\b/i,
58
+ /\bgit\s+reset\s+(--hard\b|--merge\b|--keep\b)/i,
59
+ /\bgit\s+clean\s+-[^\n\r]*f/i,
60
+ /\bgit\s+checkout\s+--\b/i,
61
+ /\bgit\s+restore\s+--source=/i,
62
+ ] as const;
63
+
64
+ const normalizeUserInput = (value: unknown): string =>
65
+ typeof value === "string" ? value.trim().toLowerCase() : "";
66
+
67
+ const containsAny = (text: string, patterns: readonly string[]): boolean =>
68
+ patterns.some((pattern) => text.includes(pattern));
69
+
70
+ const hasExplicitDestructivePermission = (userInput: unknown): boolean => {
71
+ const normalized = normalizeUserInput(userInput);
72
+ if (!normalized) return false;
73
+ if (containsAny(normalized, DESTRUCTIVE_NEGATION_PATTERNS)) return false;
74
+ return containsAny(normalized, EXPLICIT_DESTRUCTIVE_REQUEST_PATTERNS);
75
+ };
76
+
77
+ const buildCombinedShellInput = (args: {
78
+ command?: unknown;
79
+ input?: unknown;
80
+ }): string =>
81
+ [
82
+ typeof args.command === "string" ? args.command : "",
83
+ typeof args.input === "string" ? args.input : "",
84
+ ]
85
+ .filter(Boolean)
86
+ .join("\n");
87
+
88
+ export function isDestructiveShellCommand(args: {
89
+ command?: unknown;
90
+ input?: unknown;
91
+ }): boolean {
92
+ const combined = buildCombinedShellInput(args);
93
+ if (!combined.trim()) return false;
94
+ return SHELL_DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(combined));
95
+ }
96
+
97
+ export function evaluateShellCommandPolicy(args: {
98
+ command?: unknown;
99
+ input?: unknown;
100
+ userInput?: unknown;
101
+ }): ShellCommandPolicyResult {
102
+ if (
103
+ isDestructiveShellCommand(args) &&
104
+ !hasExplicitDestructivePermission(args.userInput)
105
+ ) {
106
+ return {
107
+ verdict: "forbidden",
108
+ code: "destructive_action_requires_confirmation",
109
+ reason:
110
+ "当前运行默认禁止自动执行可能删除用户内容的 shell 命令。只有当用户在当前请求里明确要求删除/清理时,才能继续;否则请停止并先说明限制。",
111
+ policy: {
112
+ capability: "destructive_action",
113
+ target: "shell_command",
114
+ detail: "execShell destructive command",
115
+ },
116
+ };
117
+ }
118
+
119
+ return { verdict: "allowed" };
120
+ }
@@ -20,6 +20,10 @@ export type LocalCliExecutor = (
20
20
  export type LocalRuntimeProbeResult = {
21
21
  ok: boolean;
22
22
  dbPath: string;
23
+ authorityDriver?: string;
24
+ authorityEndpoint?: string;
25
+ authorityMetadataPath?: string;
26
+ authorityHealthPath?: string;
23
27
  agentFound: boolean;
24
28
  agentKey?: string;
25
29
  error?: string;
@@ -8,6 +8,7 @@ import { runAgentPullCommand } from "./agentPullCommand";
8
8
  import {
9
9
  runAgentReadCommand,
10
10
  runAgentUpdateCommand,
11
+ runAgentCreateCommand,
11
12
  } from "./agentRecordCommands";
12
13
  import { runAgentRunCommand } from "./agentRunCommand";
13
14
  import {
@@ -21,6 +22,7 @@ export function getAgentInternalCommandEntries(): CommandEntry[] {
21
22
  return [
22
23
  createEnvScriptDirCommand(["chat"], "Chat with an agent", runAgentRunCommand),
23
24
  createEnvCommand(["agent", "list"], "List owned agents", runAgentListCommand),
25
+ createEnvCommand(["agent", "create"], "Create a new agent", runAgentCreateCommand),
24
26
  createEnvCommand(["agent", "pull"], "Cache an agent for local runs", runAgentPullCommand),
25
27
  createEnvCommand(["agent", "read"], "Read a single agent", runAgentReadCommand),
26
28
  createEnvScriptDirCommand(["agent", "run"], "Run an agent", runAgentRunCommand),
@@ -1,6 +1,8 @@
1
1
  import { getReadableCliDb, type AgentCommandDeps } from "./agentCommandSupport";
2
2
  import {
3
+ decorateAgentsWithPublicStatusAcrossServers,
3
4
  listLocalCachedAgents,
5
+ listRemoteAgentsAcrossServers,
4
6
  listRemoteAgents,
5
7
  parseAgentListArgs,
6
8
  type ListedAgent,
@@ -9,11 +11,15 @@ import {
9
11
  queryUserRecords,
10
12
  readDbRecord,
11
13
  } from "./agentRecordHelpers";
14
+ import { buildSpaceLookup, getSpaceContentKeys } from "./cliSpaceHelpers";
12
15
  import {
13
16
  parseUserIdFromAuthToken,
17
+ readOption,
14
18
  resolveAuthToken,
19
+ resolveServerCandidates,
15
20
  resolveServerUrl,
16
21
  } from "./cliEnvHelpers";
22
+ import { readLiveDbRecordAfterTombstoneMerge } from "./globalRecordOperations";
17
23
 
18
24
  export async function runAgentListCommand(
19
25
  args: string[],
@@ -22,8 +28,9 @@ export async function runAgentListCommand(
22
28
  const env = deps.env ?? process.env;
23
29
  const output = deps.output ?? process.stdout;
24
30
  const { wantJson, publicOnly, idsOnly, includeLegacy } = parseAgentListArgs(args);
31
+ const spaceInput = readOption(args, "--space") ?? readOption(args, "--space-id");
25
32
 
26
- const authToken = resolveAuthToken(env);
33
+ const authToken = resolveAuthToken(args, env);
27
34
  if (!authToken) {
28
35
  output.write("[nolo] agent list requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
29
36
  return 1;
@@ -35,34 +42,79 @@ export async function runAgentListCommand(
35
42
  return 1;
36
43
  }
37
44
 
38
- const db = deps.db ?? await getReadableCliDb(output);
39
45
  const fetchImpl = deps.fetchImpl ?? fetch;
40
46
  const fallbackFetchImpl = deps.fallbackFetchImpl;
47
+ const serverUrl = resolveServerUrl(args, env);
48
+ const serverUrls = resolveServerCandidates(args, env, serverUrl);
41
49
 
42
50
  try {
43
51
  let agents: ListedAgent[];
44
- let source: "local-cache" | "remote-cache";
52
+ let source: "local-cache" | "remote-cache" | "global-cache";
53
+ let serverFailures: Array<{ serverUrl: string; error: string }> = [];
45
54
  try {
46
- agents = await listLocalCachedAgents({ db, userId });
47
- source = "local-cache";
48
- } catch {
49
- const serverUrl = resolveServerUrl(env);
50
- agents = await listRemoteAgents({
55
+ const remoteResult = await listRemoteAgentsAcrossServers({
51
56
  authToken,
52
57
  fallbackFetchImpl,
53
58
  fetchImpl,
54
59
  includeLegacy,
55
- serverUrl,
60
+ serverUrls,
56
61
  userId,
57
- queryUserRecords,
58
- readDbRecord,
59
62
  });
60
- source = "remote-cache";
63
+ agents = remoteResult.agents;
64
+ serverFailures = remoteResult.failures;
65
+ source = "global-cache";
66
+ } catch {
67
+ try {
68
+ const db = deps.db ?? await getReadableCliDb(output);
69
+ agents = await listLocalCachedAgents({ db, userId });
70
+ source = "local-cache";
71
+ } catch {
72
+ agents = await listRemoteAgents({
73
+ authToken,
74
+ fallbackFetchImpl,
75
+ fetchImpl,
76
+ includeLegacy,
77
+ serverUrl,
78
+ userId,
79
+ queryUserRecords,
80
+ readDbRecord,
81
+ });
82
+ source = "remote-cache";
83
+ }
61
84
  }
62
85
 
63
86
  if (!includeLegacy) {
64
87
  agents = agents.filter((agent) => !agent.privateKey.startsWith("cybot-"));
65
88
  }
89
+ let resolvedSpaceId: string | null = null;
90
+ if (spaceInput) {
91
+ const { spaceId, spaceKey } = buildSpaceLookup(spaceInput);
92
+ resolvedSpaceId = spaceId;
93
+ const spaceRead = await readLiveDbRecordAfterTombstoneMerge({
94
+ authToken,
95
+ dbKey: spaceKey,
96
+ fallbackFetchImpl,
97
+ fetchImpl,
98
+ serverUrls,
99
+ });
100
+ serverFailures = [...serverFailures, ...spaceRead.failures];
101
+ const spaceRecord = spaceRead.record;
102
+ const spaceContentKeys = getSpaceContentKeys(spaceRecord);
103
+ agents = agents.filter((agent) =>
104
+ spaceContentKeys.has(agent.privateKey) ||
105
+ spaceContentKeys.has(agent.publicKey) ||
106
+ spaceContentKeys.has(agent.id)
107
+ );
108
+ }
109
+ if (source === "global-cache") {
110
+ await decorateAgentsWithPublicStatusAcrossServers({
111
+ agents,
112
+ authToken,
113
+ fallbackFetchImpl,
114
+ fetchImpl,
115
+ serverUrls,
116
+ });
117
+ }
66
118
  if (publicOnly) {
67
119
  agents = agents.filter((agent) => agent.publicRecordExists);
68
120
  }
@@ -75,6 +127,9 @@ export async function runAgentListCommand(
75
127
  if (wantJson) {
76
128
  output.write(JSON.stringify({
77
129
  userId,
130
+ ...(resolvedSpaceId ? { spaceId: resolvedSpaceId } : {}),
131
+ targetServers: serverUrls,
132
+ ...(serverFailures.length ? { serverFailures } : {}),
78
133
  total: agents.length,
79
134
  publicCount: agents.filter((agent) => agent.publicRecordExists).length,
80
135
  source,
@@ -85,6 +140,13 @@ export async function runAgentListCommand(
85
140
  }
86
141
 
87
142
  output.write(`userId: ${userId}\n`);
143
+ if (resolvedSpaceId) {
144
+ output.write(`spaceId: ${resolvedSpaceId}\n`);
145
+ }
146
+ output.write(`targetServers: ${serverUrls.join(", ")}\n`);
147
+ if (serverFailures.length) {
148
+ output.write(`serverFailures: ${serverFailures.length}\n`);
149
+ }
88
150
  output.write(`total agents: ${agents.length}\n`);
89
151
  output.write(`public agents: ${agents.filter((agent) => agent.publicRecordExists).length}\n`);
90
152
  output.write(`source: ${source}\n`);
@@ -1,4 +1,8 @@
1
1
  import type { CliKvDb } from "./client/hybridRecordStore";
2
+ import {
3
+ listUserRecordsFromServers,
4
+ readLiveDbRecordAfterTombstoneMerge,
5
+ } from "./globalRecordOperations";
2
6
 
3
7
  export type ListedAgent = {
4
8
  id: string;
@@ -93,7 +97,7 @@ export async function listLocalCachedAgents(args: {
93
97
  }) {
94
98
  const privateRecords = new Map<string, any>();
95
99
  const publicKeys = new Set<string>();
96
- for await (const [key, value] of args.db.iterator()) {
100
+ for await (const [key, value] of args.db.iterator({ gte: "", lte: "\uffff" })) {
97
101
  if (typeof key !== "string" || !value || typeof value !== "object") continue;
98
102
  if (key.startsWith(`agent-${args.userId}-`) || key.startsWith(`cybot-${args.userId}-`)) {
99
103
  privateRecords.set(key, { ...(value as Record<string, unknown>), dbKey: key });
@@ -185,3 +189,54 @@ export async function listRemoteAgents(args: {
185
189
  );
186
190
  return agents;
187
191
  }
192
+
193
+ export async function listRemoteAgentsAcrossServers(args: {
194
+ authToken: string;
195
+ fallbackFetchImpl?: typeof fetch;
196
+ fetchImpl: typeof fetch;
197
+ includeLegacy: boolean;
198
+ serverUrls: string[];
199
+ userId: string;
200
+ }) {
201
+ const remoteResult = await listUserRecordsFromServers({
202
+ authToken: args.authToken,
203
+ fallbackFetchImpl: args.fallbackFetchImpl,
204
+ fetchImpl: args.fetchImpl,
205
+ label: "agent query",
206
+ serverUrls: args.serverUrls,
207
+ type: args.includeLegacy ? ["agent", "cybot"] : "agent",
208
+ userId: args.userId,
209
+ });
210
+ const agents = sortListedAgents(
211
+ remoteResult.records
212
+ .map((record) => normalizeListedAgent(record))
213
+ .filter((agent): agent is ListedAgent => agent != null)
214
+ );
215
+ return { agents, failures: remoteResult.failures };
216
+ }
217
+
218
+ export async function decorateAgentsWithPublicStatusAcrossServers(args: {
219
+ agents: ListedAgent[];
220
+ authToken: string;
221
+ fallbackFetchImpl?: typeof fetch;
222
+ fetchImpl: typeof fetch;
223
+ serverUrls: string[];
224
+ }) {
225
+ await Promise.all(
226
+ args.agents.map(async (agent) => {
227
+ try {
228
+ const result = await readLiveDbRecordAfterTombstoneMerge({
229
+ authToken: args.authToken,
230
+ dbKey: agent.publicKey,
231
+ fallbackFetchImpl: args.fallbackFetchImpl,
232
+ fetchImpl: args.fetchImpl,
233
+ serverUrls: args.serverUrls,
234
+ });
235
+ agent.publicRecordExists = result.record?.isPublic === true;
236
+ } catch {
237
+ agent.publicRecordExists = false;
238
+ }
239
+ })
240
+ );
241
+ return args.agents;
242
+ }
@@ -1,7 +1,7 @@
1
- import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
2
1
  import { getDefaultCliLocalRuntimeDb } from "./localRuntimeDb";
3
2
  import type { CliLocalRuntimeDb } from "./client/localRuntimeAdapter";
4
3
  import { resolveCliAgentKeyInput } from "./agentAliases";
4
+ import { resolveAuthToken, resolveServerUrl } from "./cliEnvHelpers";
5
5
 
6
6
  type EnvLike = Record<string, string | undefined>;
7
7
 
@@ -44,14 +44,6 @@ export function parseAgentPullArgs(args: string[]): ParsedAgentPullArgs | null {
44
44
  return { agentKey: resolveCliAgentKeyInput(agentKey) };
45
45
  }
46
46
 
47
- function resolveServerUrl(env: EnvLike) {
48
- return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
49
- }
50
-
51
- function resolveAuthToken(env: EnvLike) {
52
- return env.AUTH_TOKEN || env.AUTH || env.BENCHMARK_AUTH_TOKEN || "";
53
- }
54
-
55
47
  function extractToolNames(record: any) {
56
48
  if (Array.isArray(record?.toolNames)) return record.toolNames.filter((tool: unknown) => typeof tool === "string");
57
49
  if (!Array.isArray(record?.tools)) return [];
@@ -113,8 +105,11 @@ export async function runAgentPullCommand(args: string[], deps: AgentPullCommand
113
105
  return 1;
114
106
  }
115
107
 
116
- const serverUrl = resolveServerUrl(env);
117
- const authToken = resolveAuthToken(env);
108
+ const serverUrl = resolveServerUrl(args, env);
109
+ const authToken =
110
+ readFlagValue(args, "--token") ||
111
+ readFlagValue(args, "--machine-key") ||
112
+ resolveAuthToken(env, ["BENCHMARK_AUTH_TOKEN"]);
118
113
  const fetchImpl = deps.fetchImpl ?? fetch;
119
114
  const db = deps.db ?? await getDefaultCliLocalRuntimeDb();
120
115