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
@@ -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
+ }
@@ -125,10 +125,12 @@ export async function readDbRecord(args: {
125
125
  authToken: string;
126
126
  fallbackFetchImpl?: typeof fetch;
127
127
  fetchImpl: typeof fetch;
128
+ includeDeleted?: boolean;
128
129
  serverUrl: string;
129
130
  }) {
131
+ const query = args.includeDeleted ? "?includeDeleted=true" : "";
130
132
  const res = await fetchWithTransportFallback(
131
- `${args.serverUrl}/api/v1/db/read/${encodeURIComponent(args.dbKey)}`,
133
+ `${args.serverUrl}/api/v1/db/read/${encodeURIComponent(args.dbKey)}${query}`,
132
134
  {
133
135
  method: "GET",
134
136
  headers: { Authorization: `Bearer ${args.authToken}` },
@@ -146,19 +148,29 @@ export async function queryUserRecords(args: {
146
148
  authToken: string;
147
149
  fallbackFetchImpl?: typeof fetch;
148
150
  fetchImpl: typeof fetch;
151
+ limit?: number;
149
152
  serverUrl: string;
153
+ summary?: boolean;
154
+ includeDeleted?: boolean;
150
155
  userId: string;
151
- type: "agent" | "cybot";
152
- }) {
156
+ type: string | string[];
157
+ }): Promise<any[]> {
158
+ const query = typeof args.limit === "number" && args.limit > 0
159
+ ? `?limit=${Math.floor(args.limit)}`
160
+ : "";
153
161
  const res = await fetchWithTransportFallback(
154
- `${args.serverUrl}/api/v1/db/query/${encodeURIComponent(args.userId)}`,
162
+ `${args.serverUrl}/api/v1/db/query/${encodeURIComponent(args.userId)}${query}`,
155
163
  {
156
164
  method: "POST",
157
165
  headers: {
158
166
  Authorization: `Bearer ${args.authToken}`,
159
167
  "Content-Type": "application/json",
160
168
  },
161
- body: JSON.stringify({ type: args.type }),
169
+ body: JSON.stringify({
170
+ type: args.type,
171
+ ...(args.includeDeleted ? { includeDeleted: true } : {}),
172
+ ...(args.summary ? { summary: true } : {}),
173
+ }),
162
174
  },
163
175
  args
164
176
  );
@@ -173,6 +185,34 @@ export async function queryUserRecords(args: {
173
185
  : [];
174
186
  }
175
187
 
188
+ export async function deleteDbRecord(args: {
189
+ dbKey: string;
190
+ deleteOptions?: { type?: "single" | "table" | "messages" };
191
+ authToken: string;
192
+ fallbackFetchImpl?: typeof fetch;
193
+ fetchImpl: typeof fetch;
194
+ serverUrl: string;
195
+ }) {
196
+ const type = args.deleteOptions?.type;
197
+ const query =
198
+ type && type !== "single"
199
+ ? `?type=${encodeURIComponent(type)}`
200
+ : "";
201
+ const res = await fetchWithTransportFallback(
202
+ `${args.serverUrl}/api/v1/db/delete/${encodeURIComponent(args.dbKey)}${query}`,
203
+ {
204
+ method: "DELETE",
205
+ headers: { Authorization: `Bearer ${args.authToken}` },
206
+ },
207
+ args
208
+ );
209
+ const data = await res.json().catch(() => ({}));
210
+ if (!res.ok) {
211
+ throw new Error(`delete failed: HTTP ${res.status} ${JSON.stringify(data)}`);
212
+ }
213
+ return data;
214
+ }
215
+
176
216
  export async function writeAgentRecord(args: {
177
217
  agentKey: string;
178
218
  authToken: string;
@@ -205,14 +245,19 @@ export async function writeAgentRecord(args: {
205
245
 
206
246
  export async function resolveAgentRecordFromHybridStore(args: {
207
247
  agentInput: string;
248
+ cliArgs?: string[];
208
249
  env: EnvLike;
209
250
  db: CliKvDb;
210
251
  fetchImpl: typeof fetch;
211
252
  fallbackFetchImpl?: typeof fetch;
212
253
  }) {
213
254
  const agentKey = resolveCliAgentKeyInput(args.agentInput);
214
- const authToken = resolveAuthToken(args.env);
215
- const defaultServerUrl = resolveServerUrl(args.env);
255
+ const authToken = args.cliArgs
256
+ ? resolveAuthToken(args.cliArgs, args.env)
257
+ : resolveAuthToken(args.env);
258
+ const defaultServerUrl = args.cliArgs
259
+ ? resolveServerUrl(args.cliArgs, args.env)
260
+ : resolveServerUrl(args.env);
216
261
 
217
262
  for (const key of buildLocalAgentLookupKeys({
218
263
  agentRef: agentKey,
@@ -232,7 +277,11 @@ export async function resolveAgentRecordFromHybridStore(args: {
232
277
  }
233
278
 
234
279
  if (!shouldReadAgentKeyRemotely(key)) continue;
235
- for (const serverUrl of resolveServerCandidates(args.env, defaultServerUrl)) {
280
+ for (const serverUrl of (
281
+ args.cliArgs
282
+ ? resolveServerCandidates(args.cliArgs, args.env, defaultServerUrl)
283
+ : resolveServerCandidates(args.env, defaultServerUrl)
284
+ )) {
236
285
  try {
237
286
  const remoteRecord = await readAgentRecord({
238
287
  agentKey: key,
@@ -290,6 +339,8 @@ export function parseAgentUpdateArgs(args: string[]) {
290
339
  const tools = readOption(args, "--tools");
291
340
  const copyProviderFrom = readOption(args, "--copy-provider-from");
292
341
  const name = readOption(args, "--name");
342
+ const customProviderUrl = readOption(args, "--custom-provider-url");
343
+ const apiKey = readOption(args, "--api-key");
293
344
 
294
345
  const promptSources = [prompt, promptFile, promptDoc].filter(Boolean);
295
346
  if (promptSources.length > 1) {
@@ -304,6 +355,8 @@ export function parseAgentUpdateArgs(args: string[]) {
304
355
  if (promptFile) updates.prompt = readFileSync(promptFile, "utf8");
305
356
  if (tools) updates.tools = parseJsonishValue(tools);
306
357
  if (name) updates.name = name;
358
+ if (customProviderUrl) updates.customProviderUrl = customProviderUrl;
359
+ if (apiKey) updates.apiKey = apiKey;
307
360
 
308
361
  for (const entry of readRepeatedOption(args, "--field")) {
309
362
  const index = entry.indexOf("=");
@@ -322,7 +375,73 @@ export function parseAgentUpdateArgs(args: string[]) {
322
375
  return { agentInput, updates, promptDoc, copyProviderFrom };
323
376
  }
324
377
 
378
+ export async function buildCreatedAgentRecord(args: {
379
+ cliArgs?: string[];
380
+ parsed: NonNullable<ReturnType<typeof parseAgentUpdateArgs>>;
381
+ env: EnvLike;
382
+ db: CliKvDb;
383
+ fetchImpl: typeof fetch;
384
+ fallbackFetchImpl?: typeof fetch;
385
+ authToken: string;
386
+ }) {
387
+ const agentKey = resolveCliAgentKeyInput(args.parsed.agentInput);
388
+ const serverUrl = args.cliArgs ? resolveServerUrl(args.cliArgs, args.env) : resolveServerUrl(args.env);
389
+
390
+ if (args.parsed.copyProviderFrom) {
391
+ const providerSource = await resolveAgentRecordFromHybridStore({
392
+ agentInput: args.parsed.copyProviderFrom,
393
+ cliArgs: args.cliArgs,
394
+ env: args.env,
395
+ db: args.db,
396
+ fetchImpl: args.fetchImpl,
397
+ fallbackFetchImpl: args.fallbackFetchImpl,
398
+ });
399
+ if (!providerSource) {
400
+ throw new Error(`provider source agent not found: ${args.parsed.copyProviderFrom}`);
401
+ }
402
+ for (const field of PROVIDER_COPY_FIELDS) {
403
+ if (field in providerSource.record) {
404
+ args.parsed.updates[field] = providerSource.record[field];
405
+ }
406
+ }
407
+ }
408
+
409
+ if (args.parsed.promptDoc) {
410
+ const promptDoc = await readDbRecord({
411
+ dbKey: args.parsed.promptDoc,
412
+ authToken: args.authToken,
413
+ fallbackFetchImpl: args.fallbackFetchImpl,
414
+ fetchImpl: args.fetchImpl,
415
+ serverUrl,
416
+ });
417
+ const promptBody = typeof promptDoc?.content === "string" ? promptDoc.content : "";
418
+ if (!promptBody.trim()) {
419
+ throw new Error(`prompt doc is empty or unreadable: ${args.parsed.promptDoc}`);
420
+ }
421
+ args.parsed.updates.prompt = promptBody;
422
+ }
423
+
424
+ const userId = parseUserIdFromAuthToken(args.authToken);
425
+
426
+ const nextRecord = {
427
+ ...args.parsed.updates,
428
+ dbKey: agentKey,
429
+ key: agentKey,
430
+ createdAt: Date.now(),
431
+ updatedAt: Date.now(),
432
+ userId,
433
+ };
434
+
435
+ return {
436
+ agentKey,
437
+ serverUrl,
438
+ nextRecord,
439
+ updates: args.parsed.updates,
440
+ };
441
+ }
442
+
325
443
  export async function buildUpdatedAgentRecord(args: {
444
+ cliArgs?: string[];
326
445
  parsed: NonNullable<ReturnType<typeof parseAgentUpdateArgs>>;
327
446
  env: EnvLike;
328
447
  db: CliKvDb;
@@ -332,13 +451,16 @@ export async function buildUpdatedAgentRecord(args: {
332
451
  }) {
333
452
  const cached = await resolveAgentRecordFromHybridStore({
334
453
  agentInput: args.parsed.agentInput,
454
+ cliArgs: args.cliArgs,
335
455
  env: args.env,
336
456
  db: args.db,
337
457
  fetchImpl: args.fetchImpl,
338
458
  fallbackFetchImpl: args.fallbackFetchImpl,
339
459
  });
340
460
  const agentKey = cached?.agentKey ?? resolveCliAgentKeyInput(args.parsed.agentInput);
341
- const serverUrl = cached?.record?.serverOrigin ?? resolveServerUrl(args.env);
461
+ const serverUrl = cached?.record?.serverOrigin ?? (
462
+ args.cliArgs ? resolveServerUrl(args.cliArgs, args.env) : resolveServerUrl(args.env)
463
+ );
342
464
  const currentRecord = await readAgentRecord({
343
465
  agentKey,
344
466
  authToken: args.authToken,
@@ -350,6 +472,7 @@ export async function buildUpdatedAgentRecord(args: {
350
472
  if (args.parsed.copyProviderFrom) {
351
473
  const providerSource = await resolveAgentRecordFromHybridStore({
352
474
  agentInput: args.parsed.copyProviderFrom,
475
+ cliArgs: args.cliArgs,
353
476
  env: args.env,
354
477
  db: args.db,
355
478
  fetchImpl: args.fetchImpl,
@@ -19,6 +19,9 @@ export {
19
19
  resolvePlatformChatProviderConfig,
20
20
  resolveLocalToolPolicy,
21
21
  resolveOpenAiCompatibleProviderConfig,
22
+ resolveRequestedRuntimeToolNames,
23
+ evaluateShellCommandPolicy,
24
+ isDestructiveShellCommand,
22
25
  shouldUsePlatformChatProvider,
23
26
  shouldCacheHybridRemoteRecord,
24
27
  shouldFetchAgentRuntimeRecordRemotely,
@@ -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: {
@@ -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) {
@@ -0,0 +1,39 @@
1
+ export function normalizeSpaceIdInput(rawInput: string) {
2
+ const raw = rawInput.trim();
3
+ if (!raw) return "";
4
+
5
+ if (raw.startsWith("http://") || raw.startsWith("https://")) {
6
+ const url = new URL(raw);
7
+ const match = url.pathname.match(/^\/space\/([^/?#]+)/);
8
+ if (match?.[1]) return decodeURIComponent(match[1]);
9
+ }
10
+
11
+ return raw.startsWith("space-") ? raw.slice("space-".length) : raw;
12
+ }
13
+
14
+ export function buildSpaceLookup(rawInput: string) {
15
+ const spaceId = normalizeSpaceIdInput(rawInput);
16
+ return {
17
+ spaceId,
18
+ spaceKey: spaceId.startsWith("space-") ? spaceId : `space-${spaceId}`,
19
+ };
20
+ }
21
+
22
+ export function getSpaceContentKeys(spaceRecord: any): Set<string> {
23
+ const keys = new Set<string>();
24
+ const contents = spaceRecord?.contents;
25
+ if (!contents || typeof contents !== "object") return keys;
26
+
27
+ for (const [entryKey, value] of Object.entries(contents)) {
28
+ if (typeof entryKey === "string" && entryKey.trim()) {
29
+ keys.add(entryKey.trim());
30
+ }
31
+ if (value && typeof value === "object") {
32
+ const contentKey = (value as any).contentKey;
33
+ if (typeof contentKey === "string" && contentKey.trim()) {
34
+ keys.add(contentKey.trim());
35
+ }
36
+ }
37
+ }
38
+ return keys;
39
+ }
@@ -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) => {