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
@@ -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,
@@ -0,0 +1,70 @@
1
+ const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = {
2
+ createPage: "createDoc",
3
+ updatePage: "updateDoc",
4
+ update_page: "updateDoc",
5
+ create_page: "createDoc",
6
+ fetchwebpage: "fetchWebpage",
7
+ fetch_webpage: "fetchWebpage",
8
+ "fetch-page": "fetchWebpage",
9
+ exasearch: "exa_search",
10
+ "exa-search": "exa_search",
11
+ exa_search: "exa_search",
12
+ readDoc: "readDoc",
13
+ readpage: "readDoc",
14
+ read_page: "readDoc",
15
+ read_doc: "readDoc",
16
+ createdoc: "createDoc",
17
+ updateDocTool: "updateDoc",
18
+ readLocal: "readLocalFile",
19
+ read_local: "readLocalFile",
20
+ readLocalTool: "readLocalFile",
21
+ listLocal: "listLocalFiles",
22
+ list_local: "listLocalFiles",
23
+ terminalCommand: "execShell",
24
+ terminal_command: "execShell",
25
+ runCommand: "execShell",
26
+ run_command: "execShell",
27
+ runInBash: "execShell",
28
+ run_in_bash: "execShell",
29
+ executeCommand: "execShell",
30
+ execute_command: "execShell",
31
+ };
32
+
33
+ const normalizeToolName = (name: string): string =>
34
+ name.replace(/[-_]/g, "").toLowerCase();
35
+
36
+ export const canonicalizeToolName = (rawName: string): string => {
37
+ const trimmedName = rawName.trim();
38
+ if (!trimmedName) return rawName;
39
+
40
+ if (LEGACY_TOOL_NAME_ALIASES[trimmedName]) {
41
+ return LEGACY_TOOL_NAME_ALIASES[trimmedName];
42
+ }
43
+
44
+ const normalizedRawName = normalizeToolName(trimmedName);
45
+ const matchedAlias = Object.entries(LEGACY_TOOL_NAME_ALIASES).find(
46
+ ([alias]) => normalizeToolName(alias) === normalizedRawName,
47
+ );
48
+
49
+ return matchedAlias?.[1] ?? trimmedName;
50
+ };
51
+
52
+ export const canonicalizeToolNames = (toolNames: string[]): string[] =>
53
+ Array.from(
54
+ new Set(
55
+ toolNames
56
+ .filter((toolName): toolName is string => typeof toolName === "string")
57
+ .map(canonicalizeToolName),
58
+ ),
59
+ );
60
+
61
+ export const prioritizeToolNames = (
62
+ toolNames: string[],
63
+ preferredToolNames: string[],
64
+ ): string[] => {
65
+ const canonicalTools = canonicalizeToolNames(toolNames);
66
+ const preferred = new Set(canonicalizeToolNames(preferredToolNames));
67
+ const prioritized = canonicalTools.filter((toolName) => preferred.has(toolName));
68
+ const remaining = canonicalTools.filter((toolName) => !preferred.has(toolName));
69
+ return [...prioritized, ...remaining];
70
+ };
@@ -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
+ }