nolo-cli 0.1.23 → 0.1.26

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 (76) hide show
  1. package/README.md +25 -3
  2. package/agent-runtime/hybridRecordStore.ts +30 -9
  3. package/agent-runtime/localLoop.ts +12 -4
  4. package/agent-runtime/localToolPolicy.ts +22 -10
  5. package/agent-runtime/localWorkspaceTools.ts +140 -374
  6. package/agent-runtime/runtimeToolPolicy.ts +43 -2
  7. package/agent-runtime/types.ts +5 -0
  8. package/agentAliases.ts +1 -1
  9. package/agentCommandSupport.ts +77 -0
  10. package/agentInternalCommandEntries.ts +34 -0
  11. package/agentListCommands.ts +123 -0
  12. package/agentListHelpers.ts +187 -0
  13. package/agentMachineCommands.ts +402 -0
  14. package/agentRecordCommands.ts +141 -0
  15. package/agentRecordHelpers.ts +395 -0
  16. package/agentRunCommand.ts +1 -2
  17. package/agentRuntimeCommands.ts +7 -701
  18. package/ai/agent/_executeModel.ts +29 -3
  19. package/ai/agent/buildSystemPrompt.ts +17 -0
  20. package/ai/agent/cliChatClient.ts +38 -4
  21. package/ai/agent/cliExecutor.ts +304 -25
  22. package/ai/agent/createAgentSchema.ts +2 -2
  23. package/ai/agent/guidedCreation/assistant.ts +82 -0
  24. package/ai/agent/guidedCreation/capabilities.ts +84 -0
  25. package/ai/agent/guidedCreation/draft.ts +62 -0
  26. package/ai/agent/guidedCreation/types.ts +38 -0
  27. package/ai/agent/machineRunPermissions.ts +21 -3
  28. package/ai/agent/runtimeGuidance.ts +1 -1
  29. package/ai/agent/server/fetchPublicAgents.ts +6 -1
  30. package/ai/agent/streamAgentChatTurn.ts +492 -19
  31. package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
  32. package/ai/ai.locale.ts +14 -2
  33. package/ai/chat/sendOpenAICompletionsRequest.ts +28 -3
  34. package/ai/chat/sendOpenAIResponseRequest.ts +3 -2
  35. package/ai/llm/getPricing.ts +14 -0
  36. package/ai/llm/kimi.ts +3 -0
  37. package/ai/llm/modelAvatar.ts +1 -0
  38. package/ai/llm/providers.ts +1 -1
  39. package/ai/taskRun/taskRunProtocol.ts +24 -10
  40. package/ai/token/modelUsageQuery.ts +17 -0
  41. package/ai/tools/agent/agentTools.ts +21 -0
  42. package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
  43. package/ai/tools/checkEnvTool.ts +52 -20
  44. package/ai/tools/convertMarxistsBookTool.ts +103 -0
  45. package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
  46. package/ai/tools/index.ts +24 -14
  47. package/ai/tools/marxistsOfflineBook.ts +253 -0
  48. package/ai/tools/modelUsageTools.ts +4 -2
  49. package/ai/tools/readTool.ts +31 -0
  50. package/ai/tools/toolApiClient.ts +35 -1
  51. package/ai/tools/toolNameAliases.ts +13 -0
  52. package/cliCommandAdapters.ts +28 -0
  53. package/cliCommandDispatch.ts +66 -0
  54. package/cliCommandFactories.ts +34 -0
  55. package/cliCommandTypes.ts +49 -0
  56. package/cliEnvHelpers.ts +44 -0
  57. package/client/agentRun.test.ts +123 -19
  58. package/client/agentRun.ts +40 -1
  59. package/client/hybridRecordStore.test.ts +42 -0
  60. package/client/hybridRecordStore.ts +11 -0
  61. package/client/localRuntimeAdapter.test.ts +189 -21
  62. package/client/localRuntimeAdapter.ts +84 -8
  63. package/client/localRuntimeDryRun.test.ts +5 -2
  64. package/client/taskRunPrompt.ts +1 -0
  65. package/commandRegistry.ts +11 -61
  66. package/connector-experimental/capabilities.ts +1 -0
  67. package/database/server/coreDataProxy.ts +43 -26
  68. package/index.ts +54 -191
  69. package/internalCommandEntries.ts +12 -0
  70. package/machineCommands.ts +301 -32
  71. package/offlineMarxistsAgentCommand.ts +314 -0
  72. package/package.json +20 -2
  73. package/runtimeDoctorCommands.ts +118 -0
  74. package/scriptCommandEntries.ts +71 -0
  75. package/systemInternalCommandEntries.ts +71 -0
  76. package/workflowInternalCommandEntries.ts +11 -0
@@ -1,5 +1,5 @@
1
1
  import type { AgentRuntimeAgentConfig } from "./hostAdapter";
2
- import type { AgentRuntimeToolPolicy } from "./types";
2
+ import type { AgentRuntimeHost, AgentRuntimeToolPolicy } from "./types";
3
3
 
4
4
  function isRecord(value: unknown): value is Record<string, unknown> {
5
5
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -39,6 +39,7 @@ export function normalizeAgentRuntimeToolPolicy(
39
39
  : {}),
40
40
  ...(isRecord(value.workspace) ? { workspace: { ...value.workspace } } : {}),
41
41
  ...(isRecord(value.shell) ? { shell: { ...value.shell } } : {}),
42
+ ...(isRecord(value.isolation) ? { isolation: { ...value.isolation } } : {}),
42
43
  ...(isRecord(value.git) ? { git: { ...value.git } } : {}),
43
44
  ...(isRecord(value.budget) ? { budget: { ...value.budget } } : {}),
44
45
  ...(isRecord(value.audit) ? { audit: { ...value.audit } } : {}),
@@ -69,15 +70,52 @@ export function mergeAgentRuntimeToolPolicies(
69
70
  ]),
70
71
  },
71
72
  shell: mergeRecords(base?.shell, override?.shell),
73
+ isolation: mergeRecords(base?.isolation, override?.isolation),
72
74
  git: mergeRecords(base?.git, override?.git),
73
75
  budget: mergeRecords(base?.budget, override?.budget),
74
76
  audit: mergeRecords(base?.audit, override?.audit),
75
77
  };
76
78
  }
77
79
 
80
+ function policyRequestsHostedWorkspace(policy: AgentRuntimeToolPolicy | undefined) {
81
+ if (!policy) return false;
82
+ const workspaceMode = policy.workspace?.mode;
83
+ return (
84
+ policy.shell?.enabled === true ||
85
+ policy.runtimeTools?.some((tool) => tool === "execShell") ||
86
+ workspaceMode === "lease" ||
87
+ workspaceMode === "task-worktree"
88
+ );
89
+ }
90
+
91
+ function applyWebRuntimeSafetyBaseline(
92
+ policy: AgentRuntimeToolPolicy | undefined,
93
+ ): AgentRuntimeToolPolicy | undefined {
94
+ if (!policyRequestsHostedWorkspace(policy)) return policy;
95
+ return mergeAgentRuntimeToolPolicies(
96
+ policy,
97
+ {
98
+ version: 1,
99
+ shell: {
100
+ commandPolicy: "approval",
101
+ networkPolicy: "default-deny",
102
+ },
103
+ isolation: {
104
+ mode: "os-sandbox",
105
+ },
106
+ audit: {
107
+ logToolCalls: true,
108
+ logShellCommands: true,
109
+ writeToDialog: true,
110
+ },
111
+ },
112
+ );
113
+ }
114
+
78
115
  export function resolveEffectiveRuntimeToolPolicy(args: {
79
116
  agentConfig: Pick<AgentRuntimeAgentConfig, "runtimeToolPolicy" | "runtimeBinding">;
80
117
  taskRunControl?: { runtimeToolPolicy?: unknown } | null;
118
+ host?: AgentRuntimeHost;
81
119
  }): AgentRuntimeToolPolicy | undefined {
82
120
  const agentPolicy = normalizeAgentRuntimeToolPolicy(
83
121
  args.agentConfig.runtimeToolPolicy ??
@@ -88,5 +126,8 @@ export function resolveEffectiveRuntimeToolPolicy(args: {
88
126
  const taskPolicy = normalizeAgentRuntimeToolPolicy(
89
127
  args.taskRunControl?.runtimeToolPolicy,
90
128
  );
91
- return mergeAgentRuntimeToolPolicies(agentPolicy, taskPolicy);
129
+ const effectivePolicy = mergeAgentRuntimeToolPolicies(agentPolicy, taskPolicy);
130
+ return args.host === "web"
131
+ ? applyWebRuntimeSafetyBaseline(effectivePolicy)
132
+ : effectivePolicy;
92
133
  }
@@ -105,6 +105,10 @@ export type AgentRuntimeAuditPolicy = {
105
105
  writeToTask?: boolean;
106
106
  };
107
107
 
108
+ export type AgentRuntimeIsolationPolicy = {
109
+ mode?: "none" | "os-sandbox" | "container" | "gvisor" | "microvm" | "dedicated-vm";
110
+ };
111
+
108
112
  export type AgentRuntimeToolPolicy = {
109
113
  version?: 1;
110
114
  agentTools?: string[];
@@ -115,6 +119,7 @@ export type AgentRuntimeToolPolicy = {
115
119
  cwd?: string;
116
120
  };
117
121
  shell?: AgentRuntimeShellPolicy;
122
+ isolation?: AgentRuntimeIsolationPolicy;
118
123
  git?: AgentRuntimeGitPolicy;
119
124
  budget?: {
120
125
  dailyUsdLimit?: number;
package/agentAliases.ts CHANGED
@@ -13,7 +13,7 @@ export const NOLO_FULLSTACK_AGENT_KEY =
13
13
  export const NOLO_SENIOR_FULLSTACK_AGENT_ID = "01NOLOFULLSTK00000000T3XR4";
14
14
  export const NOLO_SENIOR_FULLSTACK_AGENT_KEY =
15
15
  `agent-${PLATFORM_DEMO_USER_ID}-${NOLO_SENIOR_FULLSTACK_AGENT_ID}`;
16
- export const NOLO_REVIEW_AGENT_ID = "01NOLOREVIEW0000000000R8V1";
16
+ export const NOLO_REVIEW_AGENT_ID = "01CODEXCLIMAIN00000001OK";
17
17
  export const NOLO_REVIEW_AGENT_KEY =
18
18
  `agent-${PLATFORM_DEMO_USER_ID}-${NOLO_REVIEW_AGENT_ID}`;
19
19
 
@@ -0,0 +1,77 @@
1
+ import type { MachineHeartbeat } from "./connector-experimental/protocol";
2
+ import type { CliKvDb } from "./client/hybridRecordStore";
3
+ import type { EnvLike } from "./cliEnvHelpers";
4
+
5
+ export type OutputLike = { write(chunk: string): unknown };
6
+
7
+ export type SmokeWebSocketOptions = {
8
+ headers: Record<string, string>;
9
+ onMessage: (message: string) => void | Promise<void>;
10
+ onOpen: () => void | Promise<void>;
11
+ sentMessages: string[];
12
+ };
13
+
14
+ export type LocalCliExecutor = (
15
+ provider: string,
16
+ prompt: string,
17
+ options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean }
18
+ ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
19
+
20
+ export type LocalRuntimeProbeResult = {
21
+ ok: boolean;
22
+ dbPath: string;
23
+ agentFound: boolean;
24
+ agentKey?: string;
25
+ error?: string;
26
+ };
27
+
28
+ export type AgentCommandDeps = {
29
+ env?: EnvLike;
30
+ output?: OutputLike;
31
+ db?: CliKvDb;
32
+ fetchImpl?: typeof fetch;
33
+ fallbackFetchImpl?: typeof fetch;
34
+ machineInfo?: () => MachineHeartbeat;
35
+ connectWebSocket?: (url: string, options: SmokeWebSocketOptions) => Promise<void>;
36
+ executeCli?: LocalCliExecutor;
37
+ connectorRunMessageHandler?: (
38
+ machine: MachineHeartbeat,
39
+ message: string,
40
+ pushMessage: (response: string) => void,
41
+ executeCli: LocalCliExecutor,
42
+ env: EnvLike,
43
+ fetchImpl: typeof fetch
44
+ ) => Promise<void>;
45
+ localRuntimeProbe?: (env: EnvLike) => Promise<LocalRuntimeProbeResult>;
46
+ };
47
+
48
+ async function getDefaultCliDb() {
49
+ const { getDefaultCliLocalRuntimeDb } = await import("./localRuntimeDb");
50
+ return getDefaultCliLocalRuntimeDb();
51
+ }
52
+
53
+ function createNullCliDb(): CliKvDb {
54
+ return {
55
+ get: async () => {
56
+ throw new Error("local cache unavailable");
57
+ },
58
+ put: async () => undefined,
59
+ batch: async () => undefined,
60
+ iterator: () => {
61
+ throw new Error("local cache unavailable");
62
+ },
63
+ };
64
+ }
65
+
66
+ export async function getReadableCliDb(output: OutputLike) {
67
+ try {
68
+ return await getDefaultCliDb();
69
+ } catch (error) {
70
+ output.write(
71
+ `[nolo] local agent cache unavailable; falling back to remote reads only: ${
72
+ error instanceof Error ? error.message : String(error)
73
+ }\n`
74
+ );
75
+ return createNullCliDb();
76
+ }
77
+ }
@@ -0,0 +1,34 @@
1
+ import { runAgentListCommand } from "./agentListCommands";
2
+ import {
3
+ runAgentBindCurrentCommand,
4
+ runAgentRuntimeDoctorCommand,
5
+ runAgentSmokeCurrentCommand,
6
+ } from "./agentMachineCommands";
7
+ import { runAgentPullCommand } from "./agentPullCommand";
8
+ import {
9
+ runAgentReadCommand,
10
+ runAgentUpdateCommand,
11
+ } from "./agentRecordCommands";
12
+ import { runAgentRunCommand } from "./agentRunCommand";
13
+ import {
14
+ createEnvCommand,
15
+ createEnvScriptDirCommand,
16
+ } from "./cliCommandFactories";
17
+ import type { CommandEntry } from "./cliCommandTypes";
18
+ import { runSetupOfflineMarxistsAgentCommand } from "./offlineMarxistsAgentCommand";
19
+
20
+ export function getAgentInternalCommandEntries(): CommandEntry[] {
21
+ return [
22
+ createEnvScriptDirCommand(["chat"], "Chat with an agent", runAgentRunCommand),
23
+ createEnvCommand(["agent", "list"], "List owned agents", runAgentListCommand),
24
+ createEnvCommand(["agent", "pull"], "Cache an agent for local runs", runAgentPullCommand),
25
+ createEnvCommand(["agent", "read"], "Read a single agent", runAgentReadCommand),
26
+ createEnvScriptDirCommand(["agent", "run"], "Run an agent", runAgentRunCommand),
27
+ createEnvCommand(["agent", "setup-offline-marxists"], "Create or update the Marxists.org offline book agent", runSetupOfflineMarxistsAgentCommand),
28
+ createEnvCommand(["agent", "update"], "Update agent fields", runAgentUpdateCommand),
29
+ createEnvCommand(["agent", "bind-current"], "Bind an agent to this machine", runAgentBindCurrentCommand),
30
+ createEnvCommand(["agent", "smoke-current"], "Smoke test a bound agent through this machine", runAgentSmokeCurrentCommand),
31
+ createEnvCommand(["agent", "runtime-doctor"], "Diagnose current machine runtime compatibility", runAgentRuntimeDoctorCommand),
32
+ createEnvScriptDirCommand(["agent", "chat"], "Chat with an agent", runAgentRunCommand),
33
+ ];
34
+ }
@@ -0,0 +1,123 @@
1
+ import { getReadableCliDb, type AgentCommandDeps } from "./agentCommandSupport";
2
+ import {
3
+ listLocalCachedAgents,
4
+ listRemoteAgents,
5
+ parseAgentListArgs,
6
+ type ListedAgent,
7
+ } from "./agentListHelpers";
8
+ import {
9
+ queryUserRecords,
10
+ readDbRecord,
11
+ } from "./agentRecordHelpers";
12
+ import {
13
+ parseUserIdFromAuthToken,
14
+ resolveAuthToken,
15
+ resolveServerUrl,
16
+ } from "./cliEnvHelpers";
17
+
18
+ export async function runAgentListCommand(
19
+ args: string[],
20
+ deps: AgentCommandDeps = {}
21
+ ) {
22
+ const env = deps.env ?? process.env;
23
+ const output = deps.output ?? process.stdout;
24
+ const { wantJson, publicOnly, idsOnly, includeLegacy } = parseAgentListArgs(args);
25
+
26
+ const authToken = resolveAuthToken(env);
27
+ if (!authToken) {
28
+ output.write("[nolo] agent list requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
29
+ return 1;
30
+ }
31
+
32
+ const userId = parseUserIdFromAuthToken(authToken);
33
+ if (!userId) {
34
+ output.write("[nolo] agent list could not read userId from AUTH_TOKEN.\n");
35
+ return 1;
36
+ }
37
+
38
+ const db = deps.db ?? await getReadableCliDb(output);
39
+ const fetchImpl = deps.fetchImpl ?? fetch;
40
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
41
+
42
+ try {
43
+ let agents: ListedAgent[];
44
+ let source: "local-cache" | "remote-cache";
45
+ try {
46
+ agents = await listLocalCachedAgents({ db, userId });
47
+ source = "local-cache";
48
+ } catch {
49
+ const serverUrl = resolveServerUrl(env);
50
+ agents = await listRemoteAgents({
51
+ authToken,
52
+ fallbackFetchImpl,
53
+ fetchImpl,
54
+ includeLegacy,
55
+ serverUrl,
56
+ userId,
57
+ queryUserRecords,
58
+ readDbRecord,
59
+ });
60
+ source = "remote-cache";
61
+ }
62
+
63
+ if (!includeLegacy) {
64
+ agents = agents.filter((agent) => !agent.privateKey.startsWith("cybot-"));
65
+ }
66
+ if (publicOnly) {
67
+ agents = agents.filter((agent) => agent.publicRecordExists);
68
+ }
69
+
70
+ if (idsOnly) {
71
+ output.write(`${agents.map((agent) => agent.id).join("\n")}\n`);
72
+ return 0;
73
+ }
74
+
75
+ if (wantJson) {
76
+ output.write(JSON.stringify({
77
+ userId,
78
+ total: agents.length,
79
+ publicCount: agents.filter((agent) => agent.publicRecordExists).length,
80
+ source,
81
+ agents,
82
+ }, null, 2));
83
+ output.write("\n");
84
+ return 0;
85
+ }
86
+
87
+ output.write(`userId: ${userId}\n`);
88
+ output.write(`total agents: ${agents.length}\n`);
89
+ output.write(`public agents: ${agents.filter((agent) => agent.publicRecordExists).length}\n`);
90
+ output.write(`source: ${source}\n`);
91
+ if (agents.length === 0) {
92
+ output.write("\n(no agents found)\n");
93
+ return 0;
94
+ }
95
+ for (const agent of agents) {
96
+ const status = agent.publicRecordExists ? "public" : "private";
97
+ const flagMismatch = agent.isPublicFlag !== agent.publicRecordExists
98
+ ? ` flag=${agent.isPublicFlag}`
99
+ : "";
100
+ output.write(
101
+ [
102
+ `\n[${status}] ${agent.name}`,
103
+ `id=${agent.id}`,
104
+ `type=${agent.type ?? "-"}`,
105
+ `model=${agent.model}`,
106
+ `updatedAt=${agent.updatedAt ?? "-"}`,
107
+ `privateKey=${agent.privateKey}`,
108
+ `publicKey=${agent.publicKey}${flagMismatch}`,
109
+ `tools=${agent.tools.join(", ") || "-"}`,
110
+ ].join("\n")
111
+ );
112
+ output.write("\n");
113
+ }
114
+ return 0;
115
+ } catch (error) {
116
+ output.write(
117
+ `[nolo] agent list failed: ${
118
+ error instanceof Error ? error.message : String(error)
119
+ }\n`
120
+ );
121
+ return 1;
122
+ }
123
+ }
@@ -0,0 +1,187 @@
1
+ import type { CliKvDb } from "./client/hybridRecordStore";
2
+
3
+ export type ListedAgent = {
4
+ id: string;
5
+ privateKey: string;
6
+ publicKey: string;
7
+ name: string;
8
+ model: string;
9
+ updatedAt: string | number | null;
10
+ isPublicFlag: boolean;
11
+ publicRecordExists: boolean;
12
+ type: string | null;
13
+ tools: string[];
14
+ };
15
+
16
+ function parseAgentRecordId(privateKey: string, explicitId?: string) {
17
+ if (explicitId) return explicitId;
18
+ const agentMatch = privateKey.match(/^agent-[^-]+-(.+)$/i);
19
+ if (agentMatch?.[1]) return agentMatch[1];
20
+ const cybotMatch = privateKey.match(/^cybot-[^-]+-([0-9A-HJKMNP-TV-Z]{26})$/i);
21
+ return cybotMatch?.[1] ?? "";
22
+ }
23
+
24
+ function buildCompanionKeys(privateKey: string, rawId: string, userId: string) {
25
+ if (privateKey.startsWith("cybot-")) {
26
+ return {
27
+ privateKey: `cybot-${userId}-${rawId}`,
28
+ publicKey: `cybot-pub-${rawId}`,
29
+ };
30
+ }
31
+ return {
32
+ privateKey: `agent-${userId}-${rawId}`,
33
+ publicKey: `agent-pub-${rawId}`,
34
+ };
35
+ }
36
+
37
+ export function normalizeListedAgent(record: any): ListedAgent | null {
38
+ const privateKey = typeof record?.dbKey === "string" ? record.dbKey : "";
39
+ const explicitId = typeof record?.id === "string" && record.id ? record.id : undefined;
40
+ const ownerUserId = typeof record?.userId === "string" ? record.userId : "";
41
+ const rawId = explicitId
42
+ || (ownerUserId && privateKey.startsWith(`agent-${ownerUserId}-`)
43
+ ? privateKey.slice(`agent-${ownerUserId}-`.length)
44
+ : ownerUserId && privateKey.startsWith(`cybot-${ownerUserId}-`)
45
+ ? privateKey.slice(`cybot-${ownerUserId}-`.length)
46
+ : parseAgentRecordId(privateKey, explicitId));
47
+ if (!privateKey || !rawId || !ownerUserId) return null;
48
+ const keys = buildCompanionKeys(privateKey, rawId, ownerUserId);
49
+ return {
50
+ id: rawId,
51
+ privateKey,
52
+ publicKey: keys.publicKey,
53
+ name: typeof record?.name === "string" && record.name ? record.name : "(unnamed)",
54
+ model: typeof record?.model === "string" && record.model ? record.model : "-",
55
+ updatedAt:
56
+ typeof record?.updatedAt === "string" || typeof record?.updatedAt === "number"
57
+ ? record.updatedAt
58
+ : typeof record?.createdAt === "string" || typeof record?.createdAt === "number"
59
+ ? record.createdAt
60
+ : typeof record?.created === "string" || typeof record?.created === "number"
61
+ ? record.created
62
+ : null,
63
+ isPublicFlag: !!record?.isPublic,
64
+ publicRecordExists: false,
65
+ type: typeof record?.type === "string" ? record.type : null,
66
+ tools: Array.isArray(record?.tools)
67
+ ? record.tools.filter((tool: unknown): tool is string => typeof tool === "string")
68
+ : [],
69
+ };
70
+ }
71
+
72
+ export function sortListedAgents(agents: ListedAgent[]) {
73
+ agents.sort((a, b) => {
74
+ const ta = a.updatedAt == null ? 0 : new Date(a.updatedAt).getTime();
75
+ const tb = b.updatedAt == null ? 0 : new Date(b.updatedAt).getTime();
76
+ return tb - ta;
77
+ });
78
+ return agents;
79
+ }
80
+
81
+ export function parseAgentListArgs(args: string[]) {
82
+ return {
83
+ wantJson: args.includes("--json"),
84
+ publicOnly: args.includes("--public-only"),
85
+ idsOnly: args.includes("--ids-only"),
86
+ includeLegacy: args.includes("--include-legacy"),
87
+ };
88
+ }
89
+
90
+ export async function listLocalCachedAgents(args: {
91
+ db: CliKvDb;
92
+ userId: string;
93
+ }) {
94
+ const privateRecords = new Map<string, any>();
95
+ const publicKeys = new Set<string>();
96
+ for await (const [key, value] of args.db.iterator()) {
97
+ if (typeof key !== "string" || !value || typeof value !== "object") continue;
98
+ if (key.startsWith(`agent-${args.userId}-`) || key.startsWith(`cybot-${args.userId}-`)) {
99
+ privateRecords.set(key, { ...(value as Record<string, unknown>), dbKey: key });
100
+ continue;
101
+ }
102
+ if (key.startsWith("agent-pub-") || key.startsWith("cybot-pub-")) {
103
+ publicKeys.add(key);
104
+ }
105
+ }
106
+
107
+ const agents = sortListedAgents(
108
+ [...privateRecords.values()]
109
+ .map((record) => normalizeListedAgent(record))
110
+ .filter((agent): agent is ListedAgent => agent != null)
111
+ );
112
+
113
+ for (const agent of agents) {
114
+ agent.publicRecordExists = publicKeys.has(agent.publicKey);
115
+ }
116
+ return agents;
117
+ }
118
+
119
+ async function hasReadableRecord(args: {
120
+ authToken: string;
121
+ dbKey: string;
122
+ fallbackFetchImpl?: typeof fetch;
123
+ fetchImpl: typeof fetch;
124
+ serverUrl: string;
125
+ readDbRecord: (args: {
126
+ authToken: string;
127
+ dbKey: string;
128
+ fallbackFetchImpl?: typeof fetch;
129
+ fetchImpl: typeof fetch;
130
+ serverUrl: string;
131
+ }) => Promise<any>;
132
+ }) {
133
+ try {
134
+ await args.readDbRecord(args);
135
+ return true;
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ export async function listRemoteAgents(args: {
142
+ authToken: string;
143
+ fallbackFetchImpl?: typeof fetch;
144
+ fetchImpl: typeof fetch;
145
+ includeLegacy: boolean;
146
+ serverUrl: string;
147
+ userId: string;
148
+ queryUserRecords: (args: {
149
+ authToken: string;
150
+ fallbackFetchImpl?: typeof fetch;
151
+ fetchImpl: typeof fetch;
152
+ serverUrl: string;
153
+ userId: string;
154
+ type: "agent" | "cybot";
155
+ }) => Promise<any[]>;
156
+ readDbRecord: (args: {
157
+ authToken: string;
158
+ dbKey: string;
159
+ fallbackFetchImpl?: typeof fetch;
160
+ fetchImpl: typeof fetch;
161
+ serverUrl: string;
162
+ }) => Promise<any>;
163
+ }) {
164
+ const recordGroups = [await args.queryUserRecords({ ...args, type: "agent" as const })];
165
+ if (args.includeLegacy) {
166
+ recordGroups.push(await args.queryUserRecords({ ...args, type: "cybot" as const }));
167
+ }
168
+ const agents = sortListedAgents(
169
+ recordGroups
170
+ .flat()
171
+ .map((record) => normalizeListedAgent(record))
172
+ .filter((agent): agent is ListedAgent => agent != null)
173
+ );
174
+ await Promise.all(
175
+ agents.map(async (agent) => {
176
+ agent.publicRecordExists = await hasReadableRecord({
177
+ authToken: args.authToken,
178
+ dbKey: agent.publicKey,
179
+ fallbackFetchImpl: args.fallbackFetchImpl,
180
+ fetchImpl: args.fetchImpl,
181
+ serverUrl: args.serverUrl,
182
+ readDbRecord: args.readDbRecord,
183
+ });
184
+ })
185
+ );
186
+ return agents;
187
+ }