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
@@ -0,0 +1,244 @@
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") return true;
130
+ if (agentConfig?.apiSource === "custom" || agentConfig?.customProviderUrl?.trim()) return false;
131
+ if (agentConfig?.useServerProxy === true) return true;
132
+ if (env.NOLO_LOCAL_LLM === "platform") return true;
133
+ if (env.NOLO_LOCAL_LLM === "direct") return false;
134
+ return !hasDirectOpenAiCompatibleProvider(env) && canUsePlatformChatProvider(env);
135
+ }
136
+
137
+ export function resolveProviderAuthHeaderName(args: {
138
+ endpoint: string;
139
+ apiKeyHeader?: string;
140
+ }) {
141
+ const explicitHeader = args.apiKeyHeader?.trim();
142
+ if (explicitHeader) return explicitHeader;
143
+ if (/xiaomimimo\.com/i.test(args.endpoint)) return "api-key";
144
+ return "Authorization";
145
+ }
146
+
147
+ export function buildProviderAuthHeaders(args: {
148
+ endpoint: string;
149
+ apiKey: string;
150
+ apiKeyHeader?: string;
151
+ }) {
152
+ if (!args.apiKey) return {};
153
+ const headerName = resolveProviderAuthHeaderName(args);
154
+ return headerName.toLowerCase() === "authorization"
155
+ ? { Authorization: `Bearer ${args.apiKey}` }
156
+ : { [headerName]: args.apiKey };
157
+ }
158
+
159
+ export function buildProviderExecutionPlan(args: {
160
+ agentConfig: AgentRuntimeAgentConfig;
161
+ env: EnvLike;
162
+ runtimeKind: "local" | "desktop" | "server";
163
+ }): ProviderExecutionPlan {
164
+ const { agentConfig, env } = args;
165
+ const mode = resolveAgentProviderMode(agentConfig);
166
+ const model = agentConfig.model || "gpt-4.1-mini";
167
+ const requestOptions = pickAgentRuntimeInferenceOptions(agentConfig);
168
+
169
+ if (mode === "cli") {
170
+ return {
171
+ mode,
172
+ transport: "direct",
173
+ cliProvider: agentConfig.cliProvider || agentConfig.provider || "codex",
174
+ model,
175
+ provider: agentConfig.provider || "cli",
176
+ requestOptions,
177
+ };
178
+ }
179
+
180
+ if (mode === "custom") {
181
+ const endpoint = resolveChatCompletionsEndpoint(agentConfig.customProviderUrl || resolveOpenAiCompatibleBaseUrl(env));
182
+ const apiKey = agentConfig.apiKey?.trim() || agentConfig.apiKeyFromAgentKey?.trim() || "";
183
+ const apiKeyHeader = resolveProviderAuthHeaderName({
184
+ endpoint,
185
+ apiKeyHeader: agentConfig.apiKeyHeader,
186
+ });
187
+ if (agentConfig.useServerProxy) {
188
+ return {
189
+ mode,
190
+ transport: "proxy",
191
+ model,
192
+ provider: agentConfig.provider || "custom",
193
+ endpoint,
194
+ requestOptions,
195
+ serverUrl: resolvePlatformServerUrl(env),
196
+ authToken: resolvePlatformAuthToken(env),
197
+ agentKey: agentConfig.key,
198
+ ...(agentConfig.apiSource ? { apiSource: agentConfig.apiSource } : {}),
199
+ ...(apiKey ? { apiKey } : {}),
200
+ ...(apiKeyHeader ? { apiKeyHeader } : {}),
201
+ };
202
+ }
203
+ return {
204
+ mode,
205
+ transport: "direct",
206
+ model,
207
+ provider: agentConfig.provider || "custom",
208
+ endpoint,
209
+ requestOptions,
210
+ apiKey,
211
+ ...(apiKeyHeader ? { apiKeyHeader } : {}),
212
+ };
213
+ }
214
+
215
+ const provider = agentConfig.provider || agentConfig.apiSource || "openai";
216
+ const endpoint = shouldUsePlatformProxyTransport(env, agentConfig)
217
+ ? resolvePlatformProviderEndpoint(agentConfig)
218
+ : resolveChatCompletionsEndpoint(resolveOpenAiCompatibleBaseUrl(env));
219
+ if (shouldUsePlatformProxyTransport(env, agentConfig)) {
220
+ return {
221
+ mode,
222
+ transport: "proxy",
223
+ model,
224
+ provider,
225
+ endpoint,
226
+ requestOptions,
227
+ serverUrl: resolvePlatformServerUrl(env),
228
+ authToken: resolvePlatformAuthToken(env),
229
+ agentKey: agentConfig.key,
230
+ ...(agentConfig.apiSource ? { apiSource: agentConfig.apiSource } : {}),
231
+ ...(agentConfig.apiKey?.trim() ? { apiKey: agentConfig.apiKey.trim() } : {}),
232
+ ...(agentConfig.apiKeyHeader?.trim() ? { apiKeyHeader: agentConfig.apiKeyHeader.trim() } : {}),
233
+ };
234
+ }
235
+ return {
236
+ mode,
237
+ transport: "direct",
238
+ model,
239
+ provider: agentConfig.provider || "openai-compatible",
240
+ endpoint,
241
+ requestOptions,
242
+ apiKey: resolveOpenAiCompatibleApiKey(env),
243
+ };
244
+ }
@@ -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
+ }
@@ -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),
@@ -23,7 +23,7 @@ export async function runAgentListCommand(
23
23
  const output = deps.output ?? process.stdout;
24
24
  const { wantJson, publicOnly, idsOnly, includeLegacy } = parseAgentListArgs(args);
25
25
 
26
- const authToken = resolveAuthToken(env);
26
+ const authToken = resolveAuthToken(args, env);
27
27
  if (!authToken) {
28
28
  output.write("[nolo] agent list requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
29
29
  return 1;
@@ -46,7 +46,7 @@ export async function runAgentListCommand(
46
46
  agents = await listLocalCachedAgents({ db, userId });
47
47
  source = "local-cache";
48
48
  } catch {
49
- const serverUrl = resolveServerUrl(env);
49
+ const serverUrl = resolveServerUrl(args, env);
50
50
  agents = await listRemoteAgents({
51
51
  authToken,
52
52
  fallbackFetchImpl,
@@ -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
 
@@ -2,6 +2,7 @@ import { resolveCliAgentKeyInput } from "./agentAliases";
2
2
  import { getReadableCliDb, type AgentCommandDeps } from "./agentCommandSupport";
3
3
  import {
4
4
  buildUpdatedAgentRecord,
5
+ buildCreatedAgentRecord,
5
6
  normalizeAgentRecordForOutput,
6
7
  parseAgentUpdateArgs,
7
8
  resolveAgentRecordFromHybridStore,
@@ -21,7 +22,7 @@ export async function runAgentReadCommand(
21
22
  return agentInput ? 0 : 1;
22
23
  }
23
24
 
24
- const authToken = resolveAuthToken(env);
25
+ const authToken = resolveAuthToken(args, env);
25
26
  if (!authToken) {
26
27
  output.write("[nolo] agent read requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
27
28
  return 1;
@@ -35,6 +36,7 @@ export async function runAgentReadCommand(
35
36
  try {
36
37
  const result = await resolveAgentRecordFromHybridStore({
37
38
  agentInput,
39
+ cliArgs: args,
38
40
  env,
39
41
  db,
40
42
  fetchImpl,
@@ -79,7 +81,7 @@ export async function runAgentUpdateCommand(
79
81
  return args[0] ? 0 : 1;
80
82
  }
81
83
 
82
- const authToken = resolveAuthToken(env);
84
+ const authToken = resolveAuthToken(args, env);
83
85
  if (!authToken) {
84
86
  output.write("[nolo] agent update requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
85
87
  return 1;
@@ -97,6 +99,7 @@ export async function runAgentUpdateCommand(
97
99
 
98
100
  try {
99
101
  const built = await buildUpdatedAgentRecord({
102
+ cliArgs: args,
100
103
  parsed,
101
104
  env,
102
105
  db,
@@ -139,3 +142,85 @@ export async function runAgentUpdateCommand(
139
142
  return 1;
140
143
  }
141
144
  }
145
+
146
+ export async function runAgentCreateCommand(
147
+ args: string[],
148
+ deps: AgentCommandDeps = {}
149
+ ) {
150
+ const env = deps.env ?? process.env;
151
+ const output = deps.output ?? process.stdout;
152
+ let parsed;
153
+ try {
154
+ parsed = parseAgentUpdateArgs(args);
155
+ } catch (error) {
156
+ output.write(`[nolo] agent create failed: ${error instanceof Error ? error.message : String(error)}\n`);
157
+ return 1;
158
+ }
159
+ if (!parsed) {
160
+ output.write(
161
+ "Usage: nolo agent create <agent> [--model <id>] [--cli-provider <provider>] [--api-source <source>] [--prompt <text> | --prompt-file <path> | --prompt-doc <pageKey>] [--tools <json>] [--copy-provider-from <agent>] [--name <name>] [--custom-provider-url <url>] [--provider-api-key <key>] [--field key=value]\n"
162
+ );
163
+ return args[0] ? 0 : 1;
164
+ }
165
+
166
+ const authToken = resolveAuthToken(args, env);
167
+ if (!authToken) {
168
+ output.write("[nolo] agent create requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
169
+ return 1;
170
+ }
171
+
172
+ const userId = parseUserIdFromAuthToken(authToken);
173
+ if (!userId) {
174
+ output.write("[nolo] agent create could not read userId from AUTH_TOKEN.\n");
175
+ return 1;
176
+ }
177
+
178
+ const db = deps.db ?? await getReadableCliDb(output);
179
+ const fetchImpl = deps.fetchImpl ?? fetch;
180
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
181
+
182
+ try {
183
+ const built = await buildCreatedAgentRecord({
184
+ cliArgs: args,
185
+ parsed,
186
+ env,
187
+ db,
188
+ fetchImpl,
189
+ fallbackFetchImpl,
190
+ authToken,
191
+ });
192
+
193
+ await writeAgentRecord({
194
+ agentKey: built.agentKey,
195
+ authToken,
196
+ fallbackFetchImpl,
197
+ fetchImpl,
198
+ serverUrl: built.serverUrl,
199
+ userId,
200
+ record: built.nextRecord,
201
+ });
202
+ await db.put(built.agentKey, {
203
+ ...built.nextRecord,
204
+ dbKey: built.agentKey,
205
+ key: built.nextRecord?.key || built.agentKey,
206
+ serverOrigin: built.serverUrl,
207
+ });
208
+
209
+ output.write(JSON.stringify({
210
+ ok: true,
211
+ agentKey: built.agentKey,
212
+ baseUrl: built.serverUrl,
213
+ updates: built.updates,
214
+ record: built.nextRecord,
215
+ }, null, 2));
216
+ output.write("\n");
217
+ return 0;
218
+ } catch (error) {
219
+ output.write(
220
+ `[nolo] agent create failed: ${
221
+ error instanceof Error ? error.message : String(error)
222
+ }\n`
223
+ );
224
+ return 1;
225
+ }
226
+ }