nolo-cli 0.1.19 → 0.1.21

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 (111) hide show
  1. package/README.md +9 -1
  2. package/agent-runtime/agentConfigOptions.ts +12 -0
  3. package/agent-runtime/agentRecordConfig.ts +99 -0
  4. package/agent-runtime/agentRecordKeys.ts +14 -0
  5. package/agent-runtime/dialogMessageRecord.ts +16 -0
  6. package/agent-runtime/dialogWritePlan.ts +130 -0
  7. package/agent-runtime/hostAdapter.ts +13 -0
  8. package/agent-runtime/hybridRecordStore.ts +147 -0
  9. package/agent-runtime/index.ts +69 -0
  10. package/agent-runtime/localLoop.ts +69 -5
  11. package/agent-runtime/localToolPolicy.ts +130 -0
  12. package/agent-runtime/localWorkspaceTools.ts +1532 -0
  13. package/agent-runtime/openAiCompatibleProvider.ts +70 -0
  14. package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
  15. package/agent-runtime/platformChatProvider.ts +241 -0
  16. package/agent-runtime/taskWorkspace.ts +193 -0
  17. package/agent-runtime/types.ts +1 -0
  18. package/agent-runtime/workspaceSession.ts +76 -0
  19. package/agentAliases.ts +37 -0
  20. package/agentPullCommand.ts +1 -1
  21. package/agentRunCommand.ts +278 -52
  22. package/agentRuntimeCommands.ts +354 -164
  23. package/agentRuntimeLocal.ts +38 -0
  24. package/ai/agent/agentSlice.ts +10 -0
  25. package/ai/agent/buildEditingContext.ts +5 -0
  26. package/ai/agent/buildSystemPrompt.ts +41 -18
  27. package/ai/agent/canvasEditingContext.ts +49 -0
  28. package/ai/agent/cliExecutor.ts +15 -4
  29. package/ai/agent/createAgentSchema.ts +2 -0
  30. package/ai/agent/executeToolCall.ts +3 -2
  31. package/ai/agent/hooks/usePublicAgents.ts +6 -0
  32. package/ai/agent/pageBuilderHandoffRules.ts +75 -0
  33. package/ai/agent/runAgentClientLoop.ts +4 -1
  34. package/ai/agent/runtimeGuidance.ts +19 -0
  35. package/ai/agent/server/fetchPublicAgents.ts +51 -1
  36. package/ai/agent/streamAgentChatTurn.ts +20 -2
  37. package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
  38. package/ai/chat/accumulateToolCallChunks.ts +40 -9
  39. package/ai/chat/parseApiError.ts +3 -0
  40. package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
  41. package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
  42. package/ai/chat/updateTotalUsage.ts +26 -9
  43. package/ai/llm/deepinfra.ts +51 -0
  44. package/ai/llm/getPricing.ts +6 -0
  45. package/ai/llm/kimi.ts +2 -0
  46. package/ai/llm/openrouterModels.ts +0 -135
  47. package/ai/llm/providers.ts +1 -0
  48. package/ai/llm/types.ts +8 -0
  49. package/ai/taskRun/taskRunProtocol.ts +882 -0
  50. package/ai/token/calculatePrice.ts +30 -0
  51. package/ai/token/externalToolCost.ts +49 -29
  52. package/ai/token/prepareTokenUsageData.ts +6 -1
  53. package/ai/token/serverTokenWriter.ts +4 -2
  54. package/ai/tools/agent/agentTools.ts +21 -0
  55. package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
  56. package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
  57. package/ai/tools/agent/taskRunTool.ts +112 -0
  58. package/ai/tools/applyEditTool.ts +6 -3
  59. package/ai/tools/applyLineEditsTool.ts +6 -3
  60. package/ai/tools/checkEnvTool.ts +14 -9
  61. package/ai/tools/codeSearchTool.ts +17 -5
  62. package/ai/tools/execBashTool.ts +33 -29
  63. package/ai/tools/fetchWebpageSupport.ts +24 -0
  64. package/ai/tools/fetchWebpageTool.ts +18 -5
  65. package/ai/tools/index.ts +158 -0
  66. package/ai/tools/jdProductScraperTool.ts +821 -0
  67. package/ai/tools/listFilesTool.ts +6 -3
  68. package/ai/tools/localFilesTool.ts +200 -0
  69. package/ai/tools/readFileTool.ts +6 -3
  70. package/ai/tools/searchRepoTool.ts +6 -3
  71. package/ai/tools/table/rowTools.ts +6 -1
  72. package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
  73. package/ai/tools/toolApiClient.ts +20 -6
  74. package/ai/tools/wereadGatewayTool.ts +152 -0
  75. package/ai/tools/writeFileTool.ts +6 -3
  76. package/client/agentConfigResolver.test.ts +70 -0
  77. package/client/agentConfigResolver.ts +1 -0
  78. package/client/agentRun.test.ts +430 -7
  79. package/client/agentRun.ts +504 -64
  80. package/client/hybridRecordStore.test.ts +115 -0
  81. package/client/hybridRecordStore.ts +41 -0
  82. package/client/localAgentRecords.test.ts +27 -0
  83. package/client/localAgentRecords.ts +7 -0
  84. package/client/localDialogRecords.test.ts +124 -0
  85. package/client/localDialogRecords.ts +30 -0
  86. package/client/localProviderResolver.test.ts +78 -0
  87. package/client/localProviderResolver.ts +1 -0
  88. package/client/localRuntimeAdapter.test.ts +621 -9
  89. package/client/localRuntimeAdapter.ts +275 -250
  90. package/client/localRuntimeDryRun.test.ts +116 -0
  91. package/client/localToolPolicy.ts +8 -81
  92. package/client/taskRunPrompt.ts +26 -0
  93. package/client/taskWorktree.ts +8 -0
  94. package/client/workspaceSession.test.ts +57 -0
  95. package/client/workspaceSession.ts +11 -0
  96. package/commandRegistry.ts +23 -6
  97. package/connectorRunArtifact.ts +121 -0
  98. package/database/actions/write.ts +16 -2
  99. package/database/hooks/useUserData.ts +9 -3
  100. package/database/server/dataHandlers.ts +18 -20
  101. package/database/server/emailRepository.ts +3 -3
  102. package/database/server/patch.ts +18 -10
  103. package/database/server/query.ts +43 -4
  104. package/database/server/read.ts +24 -38
  105. package/database/server/recordIdentity.ts +100 -0
  106. package/database/server/write.ts +21 -25
  107. package/index.ts +70 -33
  108. package/machineCommands.ts +318 -144
  109. package/package.json +4 -1
  110. package/tableCommands.ts +181 -0
  111. package/taskRunCommand.ts +265 -0
@@ -7,8 +7,14 @@ import {
7
7
  buildMachinePermissionPromptBlock,
8
8
  resolveMachineRunPermissionPolicy,
9
9
  } from "./ai/agent/machineRunPermissions";
10
- import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
11
- import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
10
+ import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
11
+ import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
12
+ import { resolveCliAgentKeyInput } from "./agentAliases";
13
+ import {
14
+ collectConnectorRunArtifact,
15
+ readConnectorGitHead,
16
+ resolveConnectorRunCwd,
17
+ } from "./connectorRunArtifact";
12
18
 
13
19
  type EnvLike = Record<string, string | undefined>;
14
20
  type OutputLike = { write(chunk: string): unknown };
@@ -21,7 +27,7 @@ type SmokeWebSocketOptions = {
21
27
  type LocalCliExecutor = (
22
28
  provider: string,
23
29
  prompt: string,
24
- options: { model?: string; yolo?: boolean }
30
+ options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean }
25
31
  ) => Promise<{ text: string; raw?: string; elapsed?: number }>;
26
32
  type LocalRuntimeProbeResult = {
27
33
  ok: boolean;
@@ -32,9 +38,10 @@ type LocalRuntimeProbeResult = {
32
38
  };
33
39
 
34
40
  type AgentRuntimeCommandDeps = {
35
- env?: EnvLike;
36
- output?: OutputLike;
37
- fetchImpl?: typeof fetch;
41
+ env?: EnvLike;
42
+ output?: OutputLike;
43
+ fetchImpl?: typeof fetch;
44
+ fallbackFetchImpl?: typeof fetch;
38
45
  machineInfo?: () => MachineHeartbeat;
39
46
  connectWebSocket?: (url: string, options: SmokeWebSocketOptions) => Promise<void>;
40
47
  executeCli?: LocalCliExecutor;
@@ -118,7 +125,11 @@ function readOption(args: string[], flag: string) {
118
125
  return index >= 0 ? args[index + 1] : undefined;
119
126
  }
120
127
 
121
- async function defaultExecuteCli(provider: string, prompt: string, options: { model?: string; yolo?: boolean }) {
128
+ async function defaultExecuteCli(
129
+ provider: string,
130
+ prompt: string,
131
+ options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean }
132
+ ) {
122
133
  const { executeCli } = await import("./ai/agent/cliExecutor");
123
134
  return executeCli(provider as any, prompt, options);
124
135
  }
@@ -127,16 +138,23 @@ function detectLaunchableMachineInfo() {
127
138
  return detectMachineInfo({ probeLaunchable: true });
128
139
  }
129
140
 
130
- function buildConnectorCliPrompt(agentConfig: any, userInput: string) {
141
+ function buildConnectorCliPrompt(agentConfig: any, userInput: string) {
131
142
  const policy = resolveMachineRunPermissionPolicy(agentConfig);
132
143
  return [
133
144
  typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
134
145
  buildMachinePermissionPromptBlock(policy),
135
146
  `--- User task ---\n${userInput}`,
136
- ].filter(Boolean).join("\n\n");
137
- }
138
-
139
- function requiredCapabilityForAgent(agent: any) {
147
+ ].filter(Boolean).join("\n\n");
148
+ }
149
+
150
+ function normalizeConnectorRunTimeoutMs(value: unknown): number | undefined {
151
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
152
+ return undefined;
153
+ }
154
+ return Math.floor(value);
155
+ }
156
+
157
+ function requiredCapabilityForAgent(agent: any) {
140
158
  if (agent?.apiSource !== "cli") return "";
141
159
  const cliProvider = String(agent?.cliProvider || "").trim();
142
160
  const capabilityByProvider: Record<string, string> = {
@@ -146,10 +164,26 @@ function requiredCapabilityForAgent(agent: any) {
146
164
  gemini: "gemini-cli",
147
165
  kimi: "kimi-cli",
148
166
  };
149
- return capabilityByProvider[cliProvider] ?? "";
150
- }
167
+ return capabilityByProvider[cliProvider] ?? "";
168
+ }
169
+
170
+ function classifyAgentRuntime(agent: any) {
171
+ if (agent?.apiSource === "cli") return "cli-machine";
172
+ if (agent?.apiSource === "platform" || agent?.useServerProxy) return "platform-local-loop";
173
+ if (agent?.apiSource === "local") return "local-provider-loop";
174
+ return "unknown";
175
+ }
176
+
177
+ function shouldRuntimeDoctorPass(args: {
178
+ runtimeClass: string;
179
+ requiredCapability: string;
180
+ hasCapability: boolean;
181
+ }) {
182
+ if (args.runtimeClass !== "cli-machine") return args.runtimeClass !== "unknown";
183
+ return Boolean(args.requiredCapability && args.hasCapability);
184
+ }
151
185
 
152
- function assertSmokeCompatible(agent: any, machine: MachineHeartbeat) {
186
+ function assertSmokeCompatible(agent: any, machine: MachineHeartbeat) {
153
187
  if (agent?.apiSource !== "cli") {
154
188
  throw new Error(`Agent ${agent?.name ?? agent?.dbKey ?? "unknown"} is not a CLI agent.`);
155
189
  }
@@ -164,26 +198,52 @@ function assertSmokeCompatible(agent: any, machine: MachineHeartbeat) {
164
198
  throw new Error(
165
199
  `Agent ${agent?.name ?? agent?.dbKey ?? "unknown"} requires ${requiredCapability}; current machine capabilities: ${currentCapabilities}.`
166
200
  );
167
- }
168
- }
169
-
170
- async function defaultConnectWebSocket(url: string, options: SmokeWebSocketOptions) {
201
+ }
202
+ }
203
+
204
+ const CONNECTOR_WS_KEEPALIVE_MS = 25_000;
205
+
206
+ function buildConnectorKeepaliveMessage() {
207
+ return JSON.stringify({ type: "connector.keepalive", sentAt: Date.now() });
208
+ }
209
+
210
+ async function defaultConnectWebSocket(url: string, options: SmokeWebSocketOptions) {
171
211
  const WebSocketCtor = globalThis.WebSocket;
172
212
  if (!WebSocketCtor) {
173
213
  throw new Error("WebSocket is not available in this runtime");
174
- }
175
- await new Promise<void>((resolve, reject) => {
176
- const ws = new WebSocketCtor(url, { headers: options.headers } as any);
177
- ws.addEventListener("open", () => {
178
- Promise.resolve(options.onOpen())
179
- .then(() => ws.close())
180
- .catch((error) => {
181
- ws.close();
182
- reject(error);
183
- });
184
- }, { once: true });
185
- ws.addEventListener("error", () => reject(new Error("connector websocket failed")));
186
- ws.addEventListener("close", () => resolve(), { once: true });
214
+ }
215
+ await new Promise<void>((resolve, reject) => {
216
+ const ws = new WebSocketCtor(url, { headers: options.headers } as any);
217
+ let keepalive: ReturnType<typeof setInterval> | null = null;
218
+ const clearKeepalive = () => {
219
+ if (keepalive) {
220
+ clearInterval(keepalive);
221
+ keepalive = null;
222
+ }
223
+ };
224
+ ws.addEventListener("open", () => {
225
+ keepalive = setInterval(() => {
226
+ try {
227
+ ws.send(buildConnectorKeepaliveMessage());
228
+ } catch {
229
+ clearKeepalive();
230
+ }
231
+ }, CONNECTOR_WS_KEEPALIVE_MS);
232
+ Promise.resolve(options.onOpen())
233
+ .then(() => ws.close())
234
+ .catch((error) => {
235
+ ws.close();
236
+ reject(error);
237
+ });
238
+ }, { once: true });
239
+ ws.addEventListener("error", () => {
240
+ clearKeepalive();
241
+ reject(new Error("connector websocket failed"));
242
+ });
243
+ ws.addEventListener("close", () => {
244
+ clearKeepalive();
245
+ resolve();
246
+ }, { once: true });
187
247
  ws.addEventListener("message", (event) => {
188
248
  const startIndex = options.sentMessages.length;
189
249
  Promise.resolve(options.onMessage(String(event.data))).then(() => {
@@ -212,27 +272,38 @@ async function handleSmokeConnectorMessage(
212
272
  if (agentConfig.apiSource !== "cli") {
213
273
  throw new Error("Connector can only execute CLI agents. Set the agent apiSource to cli and choose a cliProvider.");
214
274
  }
215
- const provider = String(agentConfig.cliProvider || "copilot");
216
- const policy = resolveMachineRunPermissionPolicy(agentConfig);
217
- const userInput = String(parsed.payload?.userInput ?? "");
218
- assertMachineRunAllowed(userInput, policy);
219
- const result = await executeCli(
220
- provider,
221
- buildConnectorCliPrompt(agentConfig, userInput),
222
- {
223
- model: agentConfig.model || undefined,
224
- yolo: true,
225
- }
226
- );
227
- send(JSON.stringify({
228
- type: "agent.run.result",
229
- requestId: parsed.requestId,
275
+ const provider = String(agentConfig.cliProvider || "copilot");
276
+ const policy = resolveMachineRunPermissionPolicy(agentConfig);
277
+ const userInput = String(parsed.payload?.userInput ?? "");
278
+ const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
279
+ assertMachineRunAllowed(userInput, policy);
280
+ const cwd = resolveConnectorRunCwd({ env: process.env, policy });
281
+ const baseSha = await readConnectorGitHead(cwd);
282
+ const result = await executeCli(
283
+ provider,
284
+ buildConnectorCliPrompt(agentConfig, userInput),
285
+ {
286
+ model: agentConfig.model || undefined,
287
+ timeout,
288
+ cwd,
289
+ yolo: true,
290
+ }
291
+ );
292
+ const artifacts = await collectConnectorRunArtifact({
293
+ cwd,
294
+ baseSha,
295
+ exitStatus: "completed",
296
+ });
297
+ send(JSON.stringify({
298
+ type: "agent.run.result",
299
+ requestId: parsed.requestId,
230
300
  result: {
231
- content: result.text,
232
- model: agentConfig.model ?? provider,
233
- trace: [{ role: "assistant", content: result.text }],
234
- },
235
- }));
301
+ content: result.text,
302
+ model: agentConfig.model ?? provider,
303
+ trace: [{ role: "assistant", content: result.text }],
304
+ artifacts,
305
+ },
306
+ }));
236
307
  } catch (error) {
237
308
  send(JSON.stringify({
238
309
  type: "agent.run.result",
@@ -242,54 +313,114 @@ async function handleSmokeConnectorMessage(
242
313
  }
243
314
  }
244
315
 
245
- async function readAgentRecord(args: {
246
- agentKey: string;
247
- authToken: string;
248
- fetchImpl: typeof fetch;
249
- serverUrl: string;
250
- }) {
251
- const res = await args.fetchImpl(
252
- `${args.serverUrl}/api/v1/db/read/${encodeURIComponent(args.agentKey)}`,
253
- {
254
- method: "GET",
255
- headers: { Authorization: `Bearer ${args.authToken}` },
256
- }
257
- );
258
- const data = await res.json().catch(() => ({}));
259
- if (!res.ok) {
260
- throw new Error(`read failed: HTTP ${res.status} ${JSON.stringify(data)}`);
261
- }
316
+ async function readAgentRecord(args: {
317
+ agentKey: string;
318
+ authToken: string;
319
+ fallbackFetchImpl?: typeof fetch;
320
+ fetchImpl: typeof fetch;
321
+ serverUrl: string;
322
+ }) {
323
+ const res = await fetchWithTransportFallback(
324
+ `${args.serverUrl}/api/v1/db/read/${encodeURIComponent(args.agentKey)}`,
325
+ {
326
+ method: "GET",
327
+ headers: { Authorization: `Bearer ${args.authToken}` },
328
+ },
329
+ args
330
+ );
331
+ const data = await res.json().catch(() => ({}));
332
+ if (!res.ok) {
333
+ throw new Error(`read failed: HTTP ${res.status} ${JSON.stringify(data)}`);
334
+ }
262
335
  return data?.data ?? data;
263
336
  }
264
337
 
265
- async function writeAgentRecord(args: {
266
- agentKey: string;
267
- authToken: string;
268
- fetchImpl: typeof fetch;
269
- serverUrl: string;
270
- userId: string;
271
- record: Record<string, any>;
272
- }) {
273
- const res = await args.fetchImpl(`${args.serverUrl}/api/v1/db/write/`, {
274
- method: "POST",
275
- headers: {
276
- Authorization: `Bearer ${args.authToken}`,
277
- "Content-Type": "application/json",
278
- },
338
+ async function writeAgentRecord(args: {
339
+ agentKey: string;
340
+ authToken: string;
341
+ fallbackFetchImpl?: typeof fetch;
342
+ fetchImpl: typeof fetch;
343
+ serverUrl: string;
344
+ userId: string;
345
+ record: Record<string, any>;
346
+ }) {
347
+ const res = await fetchWithTransportFallback(`${args.serverUrl}/api/v1/db/write/`, {
348
+ method: "POST",
349
+ headers: {
350
+ Authorization: `Bearer ${args.authToken}`,
351
+ "Content-Type": "application/json",
352
+ },
279
353
  body: JSON.stringify({
280
354
  customKey: args.agentKey,
281
355
  userId: args.userId,
282
356
  data: {
283
357
  ...args.record,
284
- dbKey: args.agentKey,
285
- },
286
- }),
287
- });
288
- const data = await res.json().catch(() => ({}));
289
- if (!res.ok) {
290
- throw new Error(`write failed: HTTP ${res.status} ${JSON.stringify(data)}`);
291
- }
292
- }
358
+ dbKey: args.agentKey,
359
+ },
360
+ }),
361
+ }, args);
362
+ const data = await res.json().catch(() => ({}));
363
+ if (!res.ok) {
364
+ throw new Error(`write failed: HTTP ${res.status} ${JSON.stringify(data)}`);
365
+ }
366
+ }
367
+
368
+ function shouldUseCurlTransportFallback(error: unknown) {
369
+ const message = error instanceof Error ? error.message : String(error);
370
+ return /Unable to connect|ConnectionRefused|ECONNREFUSED|Failed to connect|Was there a typo|timed out|Timeout|handshake|certificate|ECONNRESET|socket|network/i.test(message);
371
+ }
372
+
373
+ async function curlFetch(url: string, init?: RequestInit): Promise<Response> {
374
+ const method = init?.method ?? "GET";
375
+ const headers = new Headers(init?.headers ?? {});
376
+ const command = ["curl", "-sS", "-L", "-X", method];
377
+
378
+ headers.forEach((value, key) => {
379
+ command.push("-H", `${key}: ${value}`);
380
+ });
381
+
382
+ if (typeof init?.body === "string" && init.body.length > 0) {
383
+ command.push("--data", init.body);
384
+ }
385
+
386
+ command.push("-w", "\n__NOLO_STATUS__:%{http_code}", url);
387
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
388
+ const [stdout, stderr, exitCode] = await Promise.all([
389
+ new Response(proc.stdout).text(),
390
+ new Response(proc.stderr).text(),
391
+ proc.exited,
392
+ ]);
393
+ if (exitCode !== 0) {
394
+ throw new Error(stderr.trim() || `curl failed for ${url}`);
395
+ }
396
+
397
+ const marker = "\n__NOLO_STATUS__:";
398
+ const markerIndex = stdout.lastIndexOf(marker);
399
+ const body = markerIndex >= 0 ? stdout.slice(0, markerIndex) : stdout;
400
+ const statusText = markerIndex >= 0 ? stdout.slice(markerIndex + marker.length).trim() : "0";
401
+ const status = Number(statusText);
402
+ return new Response(body, {
403
+ status: Number.isFinite(status) ? status : 0,
404
+ headers: { "Content-Type": headers.get("Content-Type") ?? "application/json" },
405
+ });
406
+ }
407
+
408
+ async function fetchWithTransportFallback(
409
+ url: string,
410
+ init: RequestInit,
411
+ options: { fallbackFetchImpl?: typeof fetch; fetchImpl: typeof fetch }
412
+ ): Promise<Response> {
413
+ try {
414
+ return await options.fetchImpl(url, init);
415
+ } catch (error) {
416
+ if (!shouldUseCurlTransportFallback(error)) throw error;
417
+ if (options.fallbackFetchImpl) {
418
+ return options.fallbackFetchImpl(url, init);
419
+ }
420
+ if (options.fetchImpl !== fetch) throw error;
421
+ return curlFetch(url, init);
422
+ }
423
+ }
293
424
 
294
425
  async function heartbeatCurrentMachine(args: {
295
426
  authToken: string;
@@ -311,10 +442,10 @@ async function heartbeatCurrentMachine(args: {
311
442
  }
312
443
  }
313
444
 
314
- export async function runAgentBindCurrentCommand(
315
- args: string[],
316
- deps: AgentRuntimeCommandDeps = {}
317
- ) {
445
+ export async function runAgentBindCurrentCommand(
446
+ args: string[],
447
+ deps: AgentRuntimeCommandDeps = {}
448
+ ) {
318
449
  const env = deps.env ?? process.env;
319
450
  const output = deps.output ?? process.stdout;
320
451
  const agentKey = args[0]?.trim();
@@ -335,13 +466,14 @@ export async function runAgentBindCurrentCommand(
335
466
  return 1;
336
467
  }
337
468
 
338
- const serverUrl = resolveServerUrl(env);
339
- const fetchImpl = deps.fetchImpl ?? fetch;
340
- const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
341
-
342
- try {
343
- await heartbeatCurrentMachine({ authToken, fetchImpl, machine, serverUrl });
344
- const existing = await readAgentRecord({ agentKey, authToken, fetchImpl, serverUrl });
469
+ const serverUrl = resolveServerUrl(env);
470
+ const fetchImpl = deps.fetchImpl ?? fetch;
471
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
472
+ const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
473
+
474
+ try {
475
+ await heartbeatCurrentMachine({ authToken, fetchImpl, machine, serverUrl });
476
+ const existing = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
345
477
  const updated = {
346
478
  ...existing,
347
479
  runtimeBinding: {
@@ -354,11 +486,12 @@ export async function runAgentBindCurrentCommand(
354
486
  updatedAt: Date.now(),
355
487
  };
356
488
  await writeAgentRecord({
357
- agentKey,
358
- authToken,
359
- fetchImpl,
360
- serverUrl,
361
- userId,
489
+ agentKey,
490
+ authToken,
491
+ fallbackFetchImpl,
492
+ fetchImpl,
493
+ serverUrl,
494
+ userId,
362
495
  record: updated,
363
496
  });
364
497
  } catch (error) {
@@ -370,21 +503,73 @@ export async function runAgentBindCurrentCommand(
370
503
  return 1;
371
504
  }
372
505
 
373
- output.write(`Bound agent ${agentKey} to this machine: ${machine.name} (${machine.machineId})\n`);
374
- return 0;
375
- }
376
-
377
- export async function runAgentSmokeCurrentCommand(
378
- args: string[],
506
+ output.write(`Bound agent ${agentKey} to this machine: ${machine.name} (${machine.machineId})\n`);
507
+ return 0;
508
+ }
509
+
510
+ export async function runAgentReadCommand(
511
+ args: string[],
512
+ deps: AgentRuntimeCommandDeps = {}
513
+ ) {
514
+ const env = deps.env ?? process.env;
515
+ const output = deps.output ?? process.stdout;
516
+ const agentInput = args[0]?.trim();
517
+ if (!agentInput || agentInput === "--help" || agentInput === "-h") {
518
+ output.write("Usage: nolo agent read <agent>\n");
519
+ return agentInput ? 0 : 1;
520
+ }
521
+
522
+ const authToken = resolveAuthToken(env);
523
+ if (!authToken) {
524
+ output.write("[nolo] agent read requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
525
+ return 1;
526
+ }
527
+
528
+ const agentKey = resolveCliAgentKeyInput(agentInput);
529
+ const serverUrl = resolveServerUrl(env);
530
+ const fetchImpl = deps.fetchImpl ?? fetch;
531
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
532
+
533
+ try {
534
+ const agent = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
535
+ output.write(JSON.stringify({
536
+ agentKey,
537
+ baseUrl: serverUrl,
538
+ name: agent?.name,
539
+ greeting: agent?.greeting,
540
+ model: agent?.model,
541
+ provider: agent?.provider ?? agent?.apiSource ?? null,
542
+ customProviderUrl: agent?.customProviderUrl ?? null,
543
+ tools: agent?.tools ?? [],
544
+ isPublic: agent?.isPublic,
545
+ authUserId: parseUserIdFromAuthToken(authToken),
546
+ userId: agent?.userId,
547
+ record: agent,
548
+ }, null, 2));
549
+ output.write("\n");
550
+ return 0;
551
+ } catch (error) {
552
+ output.write(
553
+ `[nolo] agent read failed: ${
554
+ error instanceof Error ? error.message : String(error)
555
+ }\n`
556
+ );
557
+ return 1;
558
+ }
559
+ }
560
+
561
+ export async function runAgentSmokeCurrentCommand(
562
+ args: string[],
379
563
  deps: AgentRuntimeCommandDeps = {}
380
564
  ) {
381
565
  const env = deps.env ?? process.env;
382
566
  const output = deps.output ?? process.stdout;
383
- const agentKey = args[0]?.trim();
384
- if (!agentKey || agentKey === "--help" || agentKey === "-h") {
385
- output.write("Usage: nolo agent smoke-current <agentKey> --msg \"hello\"\n");
386
- return agentKey ? 0 : 1;
387
- }
567
+ const agentInput = args[0]?.trim();
568
+ if (!agentInput || agentInput === "--help" || agentInput === "-h") {
569
+ output.write("Usage: nolo agent smoke-current <agent> --msg \"hello\"\n");
570
+ return agentInput ? 0 : 1;
571
+ }
572
+ const agentKey = resolveCliAgentKeyInput(agentInput);
388
573
 
389
574
  const authToken = resolveAuthToken(env);
390
575
  if (!authToken) {
@@ -397,22 +582,24 @@ export async function runAgentSmokeCurrentCommand(
397
582
  return 1;
398
583
  }
399
584
 
400
- const userInput = readOption(args, "--msg") ?? "Smoke test from nolo connector.";
401
- const serverUrl = resolveServerUrl(env);
402
- const fetchImpl = deps.fetchImpl ?? fetch;
403
- const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
585
+ const userInput = readOption(args, "--msg") ?? "Smoke test from nolo connector.";
586
+ const serverUrl = resolveServerUrl(env);
587
+ const fetchImpl = deps.fetchImpl ?? fetch;
588
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
589
+ const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
404
590
  const sentMessages: string[] = [];
405
591
 
406
- try {
407
- await heartbeatCurrentMachine({ authToken, fetchImpl, machine, serverUrl });
408
- const existing = await readAgentRecord({ agentKey, authToken, fetchImpl, serverUrl });
592
+ try {
593
+ await heartbeatCurrentMachine({ authToken, fetchImpl, machine, serverUrl });
594
+ const existing = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
409
595
  assertSmokeCompatible(existing, machine);
410
596
  await writeAgentRecord({
411
- agentKey,
412
- authToken,
413
- fetchImpl,
414
- serverUrl,
415
- userId,
597
+ agentKey,
598
+ authToken,
599
+ fallbackFetchImpl,
600
+ fetchImpl,
601
+ serverUrl,
602
+ userId,
416
603
  record: {
417
604
  ...existing,
418
605
  runtimeBinding: {
@@ -477,16 +664,17 @@ export async function runAgentSmokeCurrentCommand(
477
664
  }
478
665
 
479
666
  export async function runAgentRuntimeDoctorCommand(
480
- args: string[],
481
- deps: AgentRuntimeCommandDeps = {}
482
- ) {
667
+ args: string[],
668
+ deps: AgentRuntimeCommandDeps = {}
669
+ ) {
483
670
  const env = deps.env ?? process.env;
484
671
  const output = deps.output ?? process.stdout;
485
- const agentKey = args[0]?.trim();
486
- if (!agentKey || agentKey === "--help" || agentKey === "-h") {
487
- output.write("Usage: nolo agent runtime-doctor <agentKey>\n");
488
- return agentKey ? 0 : 1;
489
- }
672
+ const agentInput = args[0]?.trim();
673
+ if (!agentInput || agentInput === "--help" || agentInput === "-h") {
674
+ output.write("Usage: nolo agent runtime-doctor <agentKey>\n");
675
+ return agentInput ? 0 : 1;
676
+ }
677
+ const agentKey = resolveCliAgentKeyInput(agentInput);
490
678
 
491
679
  const authToken = resolveAuthToken(env);
492
680
  if (!authToken) {
@@ -494,31 +682,33 @@ export async function runAgentRuntimeDoctorCommand(
494
682
  return 1;
495
683
  }
496
684
 
497
- const serverUrl = resolveServerUrl(env);
498
- const fetchImpl = deps.fetchImpl ?? fetch;
499
- const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
500
-
501
- try {
502
- const agent = await readAgentRecord({ agentKey, authToken, fetchImpl, serverUrl });
503
- const requiredCapability = requiredCapabilityForAgent(agent);
504
- const isBoundToCurrent = agent?.runtimeBinding?.machineId === machine.machineId;
505
- const hasCapability = requiredCapability
506
- ? machine.capabilities.includes(requiredCapability)
507
- : false;
508
- output.write(`Agent runtime doctor: ${agent?.name ?? agentKey}\n`);
509
- output.write(`Agent key: ${agentKey}\n`);
510
- output.write(`API source: ${agent?.apiSource ?? "unknown"}\n`);
511
- output.write(`CLI provider: ${agent?.cliProvider ?? "none"}\n`);
512
- output.write(`Required capability: ${requiredCapability || "none"}\n`);
513
- output.write(`Current machine: ${machine.name} (${machine.machineId})\n`);
514
- output.write(`Current machine capabilities: ${machine.capabilities.length ? machine.capabilities.join(", ") : "none"}\n`);
515
- output.write(`Current machine binding: ${isBoundToCurrent ? "yes" : "no"}\n`);
516
- output.write(`Current machine has required capability: ${hasCapability ? "yes" : "no"}\n`);
517
-
518
- if (agent?.apiSource !== "cli") return 1;
519
- if (!requiredCapability || !hasCapability) return 1;
520
- return 0;
521
- } catch (error) {
685
+ const serverUrl = resolveServerUrl(env);
686
+ const fetchImpl = deps.fetchImpl ?? fetch;
687
+ const fallbackFetchImpl = deps.fallbackFetchImpl;
688
+ const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
689
+
690
+ try {
691
+ const agent = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
692
+ const requiredCapability = requiredCapabilityForAgent(agent);
693
+ const runtimeClass = classifyAgentRuntime(agent);
694
+ const isBoundToCurrent = agent?.runtimeBinding?.machineId === machine.machineId;
695
+ const hasCapability = requiredCapability
696
+ ? machine.capabilities.includes(requiredCapability)
697
+ : false;
698
+ output.write(`Agent runtime doctor: ${agent?.name ?? agentKey}\n`);
699
+ if (agentInput !== agentKey) output.write(`Agent input: ${agentInput}\n`);
700
+ output.write(`Agent key: ${agentKey}\n`);
701
+ output.write(`Runtime class: ${runtimeClass}\n`);
702
+ output.write(`API source: ${agent?.apiSource ?? "unknown"}\n`);
703
+ output.write(`CLI provider: ${agent?.cliProvider ?? "none"}\n`);
704
+ output.write(`Required capability: ${requiredCapability || "none"}\n`);
705
+ output.write(`Current machine: ${machine.name} (${machine.machineId})\n`);
706
+ output.write(`Current machine capabilities: ${machine.capabilities.length ? machine.capabilities.join(", ") : "none"}\n`);
707
+ output.write(`Current machine binding: ${runtimeClass === "cli-machine" ? (isBoundToCurrent ? "yes" : "no") : "not required"}\n`);
708
+ output.write(`Current machine has required capability: ${requiredCapability ? (hasCapability ? "yes" : "no") : "not required"}\n`);
709
+
710
+ return shouldRuntimeDoctorPass({ runtimeClass, requiredCapability, hasCapability }) ? 0 : 1;
711
+ } catch (error) {
522
712
  output.write(
523
713
  `[nolo] agent runtime-doctor failed: ${
524
714
  error instanceof Error ? error.message : String(error)