nolo-cli 0.1.18 → 0.1.20
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.
- package/README.md +9 -1
- package/agent-runtime/agentConfigOptions.ts +12 -0
- package/agent-runtime/agentRecordConfig.ts +99 -0
- package/agent-runtime/agentRecordKeys.ts +14 -0
- package/agent-runtime/dialogMessageRecord.ts +16 -0
- package/agent-runtime/dialogWritePlan.ts +130 -0
- package/agent-runtime/hostAdapter.ts +14 -0
- package/agent-runtime/hybridRecordStore.ts +147 -0
- package/agent-runtime/index.ts +69 -0
- package/agent-runtime/localLoop.ts +78 -6
- package/agent-runtime/localToolPolicy.ts +130 -0
- package/agent-runtime/localWorkspaceTools.ts +1532 -0
- package/agent-runtime/openAiCompatibleProvider.ts +70 -0
- package/agent-runtime/openAiCompatibleProviderConfig.ts +38 -0
- package/agent-runtime/platformChatProvider.ts +241 -0
- package/agent-runtime/taskWorkspace.ts +193 -0
- package/agent-runtime/types.ts +2 -0
- package/agent-runtime/workspaceSession.ts +76 -0
- package/agentAliases.ts +37 -0
- package/agentPullCommand.ts +1 -1
- package/agentRunCommand.ts +289 -54
- package/agentRuntimeCommands.ts +354 -164
- package/agentRuntimeLocal.ts +38 -0
- package/ai/agent/agentSlice.ts +10 -0
- package/ai/agent/buildEditingContext.ts +5 -0
- package/ai/agent/buildSystemPrompt.ts +41 -18
- package/ai/agent/canvasEditingContext.ts +49 -0
- package/ai/agent/cliExecutor.ts +15 -4
- package/ai/agent/createAgentSchema.ts +2 -0
- package/ai/agent/executeToolCall.ts +3 -2
- package/ai/agent/hooks/usePublicAgents.ts +6 -0
- package/ai/agent/pageBuilderHandoffRules.ts +75 -0
- package/ai/agent/runAgentClientLoop.ts +4 -1
- package/ai/agent/runtimeGuidance.ts +19 -0
- package/ai/agent/server/fetchPublicAgents.ts +51 -1
- package/ai/agent/streamAgentChatTurn.ts +20 -2
- package/ai/agent/streamAgentChatTurnUtils.ts +60 -16
- package/ai/chat/accumulateToolCallChunks.ts +40 -9
- package/ai/chat/parseApiError.ts +3 -0
- package/ai/chat/sendOpenAICompletionsRequest.native.ts +23 -10
- package/ai/chat/sendOpenAICompletionsRequest.ts +13 -1
- package/ai/chat/updateTotalUsage.ts +26 -9
- package/ai/llm/deepinfra.ts +51 -0
- package/ai/llm/getPricing.ts +6 -0
- package/ai/llm/kimi.ts +2 -0
- package/ai/llm/openrouterModels.ts +0 -135
- package/ai/llm/providers.ts +1 -0
- package/ai/llm/types.ts +8 -0
- package/ai/taskRun/taskRunProtocol.ts +823 -0
- package/ai/token/calculatePrice.ts +30 -0
- package/ai/token/externalToolCost.ts +49 -29
- package/ai/token/prepareTokenUsageData.ts +6 -1
- package/ai/token/serverTokenWriter.ts +4 -2
- package/ai/tools/agent/agentTools.ts +21 -0
- package/ai/tools/agent/presets/appBuilderPreset.ts +7 -0
- package/ai/tools/agent/streamParallelAgentsTool.ts +2 -1
- package/ai/tools/agent/taskRunTool.ts +112 -0
- package/ai/tools/applyEditTool.ts +6 -3
- package/ai/tools/applyLineEditsTool.ts +6 -3
- package/ai/tools/checkEnvTool.ts +14 -9
- package/ai/tools/codeSearchTool.ts +17 -5
- package/ai/tools/execBashTool.ts +33 -29
- package/ai/tools/fetchWebpageSupport.ts +24 -0
- package/ai/tools/fetchWebpageTool.ts +18 -5
- package/ai/tools/index.ts +158 -0
- package/ai/tools/jdProductScraperTool.ts +821 -0
- package/ai/tools/listFilesTool.ts +6 -3
- package/ai/tools/localFilesTool.ts +200 -0
- package/ai/tools/readFileTool.ts +6 -3
- package/ai/tools/searchRepoTool.ts +6 -3
- package/ai/tools/table/rowTools.ts +6 -1
- package/ai/tools/taobaoTmallProductScraperTool.ts +49 -0
- package/ai/tools/toolApiClient.ts +20 -6
- package/ai/tools/wereadGatewayTool.ts +152 -0
- package/ai/tools/writeFileTool.ts +6 -3
- package/client/agentConfigResolver.test.ts +70 -0
- package/client/agentConfigResolver.ts +1 -0
- package/client/agentRun.test.ts +361 -7
- package/client/agentRun.ts +449 -63
- package/client/hybridRecordStore.test.ts +115 -0
- package/client/hybridRecordStore.ts +41 -0
- package/client/localAgentRecords.test.ts +27 -0
- package/client/localAgentRecords.ts +7 -0
- package/client/localDialogRecords.test.ts +124 -0
- package/client/localDialogRecords.ts +30 -0
- package/client/localProviderResolver.test.ts +78 -0
- package/client/localProviderResolver.ts +1 -0
- package/client/localRuntimeAdapter.test.ts +813 -20
- package/client/localRuntimeAdapter.ts +279 -232
- package/client/localRuntimeDryRun.test.ts +116 -0
- package/client/localToolPolicy.ts +8 -81
- package/client/taskRunPrompt.ts +26 -0
- package/client/taskWorktree.ts +8 -0
- package/client/workspaceSession.test.ts +57 -0
- package/client/workspaceSession.ts +11 -0
- package/commandRegistry.ts +23 -6
- package/connectorRunArtifact.ts +121 -0
- package/database/actions/write.ts +16 -2
- package/database/hooks/useUserData.ts +9 -3
- package/database/server/dataHandlers.ts +18 -20
- package/database/server/emailRepository.ts +3 -3
- package/database/server/patch.ts +18 -10
- package/database/server/query.ts +43 -4
- package/database/server/read.ts +24 -38
- package/database/server/recordIdentity.ts +100 -0
- package/database/server/write.ts +21 -25
- package/index.ts +70 -33
- package/machineCommands.ts +318 -144
- package/package.json +4 -1
- package/tableCommands.ts +181 -0
- package/taskRunCommand.ts +237 -0
package/agentRuntimeCommands.ts
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
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
|
|
384
|
-
if (!
|
|
385
|
-
output.write("Usage: nolo agent smoke-current <
|
|
386
|
-
return
|
|
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
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
|
486
|
-
if (!
|
|
487
|
-
output.write("Usage: nolo agent runtime-doctor <agentKey>\n");
|
|
488
|
-
return
|
|
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
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const
|
|
504
|
-
const
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
output.write(`
|
|
511
|
-
output.write(`
|
|
512
|
-
output.write(`
|
|
513
|
-
output.write(`
|
|
514
|
-
output.write(`
|
|
515
|
-
output.write(`
|
|
516
|
-
output.write(`
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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)
|