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.
- package/README.md +25 -3
- package/agent-runtime/hybridRecordStore.ts +30 -9
- package/agent-runtime/localLoop.ts +12 -4
- package/agent-runtime/localToolPolicy.ts +22 -10
- package/agent-runtime/localWorkspaceTools.ts +140 -374
- package/agent-runtime/runtimeToolPolicy.ts +43 -2
- package/agent-runtime/types.ts +5 -0
- package/agentAliases.ts +1 -1
- package/agentCommandSupport.ts +77 -0
- package/agentInternalCommandEntries.ts +34 -0
- package/agentListCommands.ts +123 -0
- package/agentListHelpers.ts +187 -0
- package/agentMachineCommands.ts +402 -0
- package/agentRecordCommands.ts +141 -0
- package/agentRecordHelpers.ts +395 -0
- package/agentRunCommand.ts +1 -2
- package/agentRuntimeCommands.ts +7 -701
- package/ai/agent/_executeModel.ts +29 -3
- package/ai/agent/buildSystemPrompt.ts +17 -0
- package/ai/agent/cliChatClient.ts +38 -4
- package/ai/agent/cliExecutor.ts +304 -25
- package/ai/agent/createAgentSchema.ts +2 -2
- package/ai/agent/guidedCreation/assistant.ts +82 -0
- package/ai/agent/guidedCreation/capabilities.ts +84 -0
- package/ai/agent/guidedCreation/draft.ts +62 -0
- package/ai/agent/guidedCreation/types.ts +38 -0
- package/ai/agent/machineRunPermissions.ts +21 -3
- package/ai/agent/runtimeGuidance.ts +1 -1
- package/ai/agent/server/fetchPublicAgents.ts +6 -1
- package/ai/agent/streamAgentChatTurn.ts +492 -19
- package/ai/agent/web/useGuidedAgentCreation.ts +217 -0
- package/ai/ai.locale.ts +14 -2
- package/ai/chat/sendOpenAICompletionsRequest.ts +28 -3
- package/ai/chat/sendOpenAIResponseRequest.ts +3 -2
- package/ai/llm/getPricing.ts +14 -0
- package/ai/llm/kimi.ts +3 -0
- package/ai/llm/modelAvatar.ts +1 -0
- package/ai/llm/providers.ts +1 -1
- package/ai/taskRun/taskRunProtocol.ts +24 -10
- package/ai/token/modelUsageQuery.ts +17 -0
- package/ai/tools/agent/agentTools.ts +21 -0
- package/ai/tools/agent/prepareAgentDraftTool.ts +159 -0
- package/ai/tools/checkEnvTool.ts +52 -20
- package/ai/tools/convertMarxistsBookTool.ts +103 -0
- package/ai/tools/{execBashTool.ts → execShellTool.ts} +44 -161
- package/ai/tools/index.ts +24 -14
- package/ai/tools/marxistsOfflineBook.ts +253 -0
- package/ai/tools/modelUsageTools.ts +4 -2
- package/ai/tools/readTool.ts +31 -0
- package/ai/tools/toolApiClient.ts +35 -1
- package/ai/tools/toolNameAliases.ts +13 -0
- package/cliCommandAdapters.ts +28 -0
- package/cliCommandDispatch.ts +66 -0
- package/cliCommandFactories.ts +34 -0
- package/cliCommandTypes.ts +49 -0
- package/cliEnvHelpers.ts +44 -0
- package/client/agentRun.test.ts +123 -19
- package/client/agentRun.ts +40 -1
- package/client/hybridRecordStore.test.ts +42 -0
- package/client/hybridRecordStore.ts +11 -0
- package/client/localRuntimeAdapter.test.ts +189 -21
- package/client/localRuntimeAdapter.ts +84 -8
- package/client/localRuntimeDryRun.test.ts +5 -2
- package/client/taskRunPrompt.ts +1 -0
- package/commandRegistry.ts +11 -61
- package/connector-experimental/capabilities.ts +1 -0
- package/database/server/coreDataProxy.ts +43 -26
- package/index.ts +54 -191
- package/internalCommandEntries.ts +12 -0
- package/machineCommands.ts +301 -32
- package/offlineMarxistsAgentCommand.ts +314 -0
- package/package.json +20 -2
- package/runtimeDoctorCommands.ts +118 -0
- package/scriptCommandEntries.ts +71 -0
- package/systemInternalCommandEntries.ts +71 -0
- package/workflowInternalCommandEntries.ts +11 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import type { MachineHeartbeat } from "./connector-experimental/protocol";
|
|
2
|
+
import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
|
|
3
|
+
import { resolveCliAgentKeyInput } from "./agentAliases";
|
|
4
|
+
import {
|
|
5
|
+
type AgentCommandDeps,
|
|
6
|
+
type LocalCliExecutor,
|
|
7
|
+
type SmokeWebSocketOptions,
|
|
8
|
+
} from "./agentCommandSupport";
|
|
9
|
+
import {
|
|
10
|
+
readAgentRecord,
|
|
11
|
+
writeAgentRecord,
|
|
12
|
+
} from "./agentRecordHelpers";
|
|
13
|
+
import {
|
|
14
|
+
parseUserIdFromAuthToken,
|
|
15
|
+
readOption,
|
|
16
|
+
resolveAuthToken,
|
|
17
|
+
resolveServerUrl,
|
|
18
|
+
type EnvLike,
|
|
19
|
+
} from "./cliEnvHelpers";
|
|
20
|
+
|
|
21
|
+
async function defaultExecuteCli(
|
|
22
|
+
provider: string,
|
|
23
|
+
prompt: string,
|
|
24
|
+
options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean }
|
|
25
|
+
) {
|
|
26
|
+
const { executeCli } = await import("./ai/agent/cliExecutor");
|
|
27
|
+
return executeCli(provider as any, prompt, options);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function forwardConnectorRunMessage(
|
|
31
|
+
machine: MachineHeartbeat,
|
|
32
|
+
message: string,
|
|
33
|
+
pushMessage: (response: string) => void,
|
|
34
|
+
executeCli: LocalCliExecutor,
|
|
35
|
+
env: EnvLike,
|
|
36
|
+
fetchImpl: typeof fetch
|
|
37
|
+
) {
|
|
38
|
+
const { handleConnectorRunMessage } = await import("./machineCommands");
|
|
39
|
+
return handleConnectorRunMessage(machine, message, pushMessage, executeCli, env, fetchImpl);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function detectLaunchableMachineInfo() {
|
|
43
|
+
const { detectMachineInfo } = await import("./connector-experimental/machineInfo");
|
|
44
|
+
return detectMachineInfo({ probeLaunchable: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function requiredCapabilityForAgent(agent: any) {
|
|
48
|
+
if (agent?.apiSource !== "cli") return "";
|
|
49
|
+
const cliProvider = String(agent?.cliProvider || "").trim();
|
|
50
|
+
const capabilityByProvider: Record<string, string> = {
|
|
51
|
+
codex: "codex-cli",
|
|
52
|
+
claude: "claude-code",
|
|
53
|
+
copilot: "copilot-cli",
|
|
54
|
+
gemini: "gemini-cli",
|
|
55
|
+
kimi: "kimi-cli",
|
|
56
|
+
agy: "agy-cli",
|
|
57
|
+
};
|
|
58
|
+
return capabilityByProvider[cliProvider] ?? "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function classifyAgentRuntime(agent: any) {
|
|
62
|
+
if (agent?.apiSource === "cli") return "cli-machine";
|
|
63
|
+
if (agent?.apiSource === "platform" || agent?.useServerProxy) return "platform-local-loop";
|
|
64
|
+
if (agent?.apiSource === "local") return "local-provider-loop";
|
|
65
|
+
return "unknown";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function shouldRuntimeDoctorPass(args: {
|
|
69
|
+
runtimeClass: string;
|
|
70
|
+
requiredCapability: string;
|
|
71
|
+
hasCapability: boolean;
|
|
72
|
+
}) {
|
|
73
|
+
if (args.runtimeClass !== "cli-machine") return args.runtimeClass !== "unknown";
|
|
74
|
+
return Boolean(args.requiredCapability && args.hasCapability);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertSmokeCompatible(agent: any, machine: MachineHeartbeat) {
|
|
78
|
+
if (agent?.apiSource !== "cli") {
|
|
79
|
+
throw new Error(`Agent ${agent?.name ?? agent?.dbKey ?? "unknown"} is not a CLI agent.`);
|
|
80
|
+
}
|
|
81
|
+
const requiredCapability = requiredCapabilityForAgent(agent);
|
|
82
|
+
if (!requiredCapability) {
|
|
83
|
+
throw new Error(`Agent ${agent?.name ?? agent?.dbKey ?? "unknown"} has unsupported cliProvider: ${agent?.cliProvider ?? "missing"}.`);
|
|
84
|
+
}
|
|
85
|
+
if (!machine.capabilities.includes(requiredCapability)) {
|
|
86
|
+
const currentCapabilities = machine.capabilities.length
|
|
87
|
+
? machine.capabilities.join(", ")
|
|
88
|
+
: "none";
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Agent ${agent?.name ?? agent?.dbKey ?? "unknown"} requires ${requiredCapability}; current machine capabilities: ${currentCapabilities}.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CONNECTOR_WS_KEEPALIVE_MS = 25_000;
|
|
96
|
+
|
|
97
|
+
function buildConnectorKeepaliveMessage() {
|
|
98
|
+
return JSON.stringify({ type: "connector.keepalive", sentAt: Date.now() });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function defaultConnectWebSocket(url: string, options: SmokeWebSocketOptions) {
|
|
102
|
+
const WebSocketCtor = globalThis.WebSocket;
|
|
103
|
+
if (!WebSocketCtor) {
|
|
104
|
+
throw new Error("WebSocket is not available in this runtime");
|
|
105
|
+
}
|
|
106
|
+
await new Promise<void>((resolve, reject) => {
|
|
107
|
+
const ws = new WebSocketCtor(url, { headers: options.headers } as any);
|
|
108
|
+
let keepalive: ReturnType<typeof setInterval> | null = null;
|
|
109
|
+
const clearKeepalive = () => {
|
|
110
|
+
if (keepalive) {
|
|
111
|
+
clearInterval(keepalive);
|
|
112
|
+
keepalive = null;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
ws.addEventListener("open", () => {
|
|
116
|
+
keepalive = setInterval(() => {
|
|
117
|
+
try {
|
|
118
|
+
ws.send(buildConnectorKeepaliveMessage());
|
|
119
|
+
} catch {
|
|
120
|
+
clearKeepalive();
|
|
121
|
+
}
|
|
122
|
+
}, CONNECTOR_WS_KEEPALIVE_MS);
|
|
123
|
+
Promise.resolve(options.onOpen())
|
|
124
|
+
.then(() => ws.close())
|
|
125
|
+
.catch((error) => {
|
|
126
|
+
ws.close();
|
|
127
|
+
reject(error);
|
|
128
|
+
});
|
|
129
|
+
}, { once: true });
|
|
130
|
+
ws.addEventListener("error", () => {
|
|
131
|
+
clearKeepalive();
|
|
132
|
+
reject(new Error("connector websocket failed"));
|
|
133
|
+
});
|
|
134
|
+
ws.addEventListener("close", () => {
|
|
135
|
+
clearKeepalive();
|
|
136
|
+
resolve();
|
|
137
|
+
}, { once: true });
|
|
138
|
+
ws.addEventListener("message", (event) => {
|
|
139
|
+
const startIndex = options.sentMessages.length;
|
|
140
|
+
Promise.resolve(options.onMessage(String(event.data))).then(() => {
|
|
141
|
+
for (const message of options.sentMessages.slice(startIndex)) {
|
|
142
|
+
ws.send(message);
|
|
143
|
+
}
|
|
144
|
+
}).catch(reject);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function heartbeatCurrentMachine(args: {
|
|
150
|
+
authToken: string;
|
|
151
|
+
fetchImpl: typeof fetch;
|
|
152
|
+
machine: MachineHeartbeat;
|
|
153
|
+
serverUrl: string;
|
|
154
|
+
}) {
|
|
155
|
+
const res = await args.fetchImpl(`${args.serverUrl}/api/machines/heartbeat`, {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
Authorization: `Bearer ${args.authToken}`,
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
},
|
|
161
|
+
body: JSON.stringify(args.machine),
|
|
162
|
+
});
|
|
163
|
+
const text = await res.text().catch(() => "");
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
throw new Error(`heartbeat failed: HTTP ${res.status} ${text}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function runAgentBindCurrentCommand(
|
|
170
|
+
args: string[],
|
|
171
|
+
deps: AgentCommandDeps = {}
|
|
172
|
+
) {
|
|
173
|
+
const env = deps.env ?? process.env;
|
|
174
|
+
const output = deps.output ?? process.stdout;
|
|
175
|
+
const agentKey = args[0]?.trim();
|
|
176
|
+
if (!agentKey || agentKey === "--help" || agentKey === "-h") {
|
|
177
|
+
output.write("Usage: nolo agent bind-current <agentKey>\n");
|
|
178
|
+
return agentKey ? 0 : 1;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const authToken = resolveAuthToken(env);
|
|
182
|
+
if (!authToken) {
|
|
183
|
+
output.write("[nolo] agent bind-current requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const userId = parseUserIdFromAuthToken(authToken);
|
|
188
|
+
if (!userId) {
|
|
189
|
+
output.write("[nolo] agent bind-current could not read userId from AUTH_TOKEN.\n");
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const serverUrl = resolveServerUrl(env);
|
|
194
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
195
|
+
const fallbackFetchImpl = deps.fallbackFetchImpl;
|
|
196
|
+
const machine = await (deps.machineInfo ?? detectLaunchableMachineInfo)();
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await heartbeatCurrentMachine({ authToken, fetchImpl, machine, serverUrl });
|
|
200
|
+
const existing = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
|
|
201
|
+
const updated = {
|
|
202
|
+
...existing,
|
|
203
|
+
runtimeBinding: {
|
|
204
|
+
...(existing?.runtimeBinding && typeof existing.runtimeBinding === "object"
|
|
205
|
+
? existing.runtimeBinding
|
|
206
|
+
: {}),
|
|
207
|
+
machineId: machine.machineId,
|
|
208
|
+
ownerUserId: userId,
|
|
209
|
+
},
|
|
210
|
+
updatedAt: Date.now(),
|
|
211
|
+
};
|
|
212
|
+
await writeAgentRecord({
|
|
213
|
+
agentKey,
|
|
214
|
+
authToken,
|
|
215
|
+
fallbackFetchImpl,
|
|
216
|
+
fetchImpl,
|
|
217
|
+
serverUrl,
|
|
218
|
+
userId,
|
|
219
|
+
record: updated,
|
|
220
|
+
});
|
|
221
|
+
} catch (error) {
|
|
222
|
+
output.write(
|
|
223
|
+
`[nolo] agent bind-current failed: ${
|
|
224
|
+
error instanceof Error ? error.message : String(error)
|
|
225
|
+
}\n`
|
|
226
|
+
);
|
|
227
|
+
return 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
output.write(`Bound agent ${agentKey} to this machine: ${machine.name} (${machine.machineId})\n`);
|
|
231
|
+
return 0;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function runAgentSmokeCurrentCommand(
|
|
235
|
+
args: string[],
|
|
236
|
+
deps: AgentCommandDeps = {}
|
|
237
|
+
) {
|
|
238
|
+
const env = deps.env ?? process.env;
|
|
239
|
+
const output = deps.output ?? process.stdout;
|
|
240
|
+
const agentInput = args[0]?.trim();
|
|
241
|
+
if (!agentInput || agentInput === "--help" || agentInput === "-h") {
|
|
242
|
+
output.write("Usage: nolo agent smoke-current <agent> --msg \"hello\"\n");
|
|
243
|
+
return agentInput ? 0 : 1;
|
|
244
|
+
}
|
|
245
|
+
const agentKey = resolveCliAgentKeyInput(agentInput);
|
|
246
|
+
|
|
247
|
+
const authToken = resolveAuthToken(env);
|
|
248
|
+
if (!authToken) {
|
|
249
|
+
output.write("[nolo] agent smoke-current requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
const userId = parseUserIdFromAuthToken(authToken);
|
|
253
|
+
if (!userId) {
|
|
254
|
+
output.write("[nolo] agent smoke-current could not read userId from AUTH_TOKEN.\n");
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const userInput = readOption(args, "--msg") ?? "Smoke test from nolo connector.";
|
|
259
|
+
const serverUrl = resolveServerUrl(env);
|
|
260
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
261
|
+
const fallbackFetchImpl = deps.fallbackFetchImpl;
|
|
262
|
+
const machine = await (deps.machineInfo ?? detectLaunchableMachineInfo)();
|
|
263
|
+
const sentMessages: string[] = [];
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
await heartbeatCurrentMachine({ authToken, fetchImpl, machine, serverUrl });
|
|
267
|
+
const existing = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
|
|
268
|
+
assertSmokeCompatible(existing, machine);
|
|
269
|
+
await writeAgentRecord({
|
|
270
|
+
agentKey,
|
|
271
|
+
authToken,
|
|
272
|
+
fallbackFetchImpl,
|
|
273
|
+
fetchImpl,
|
|
274
|
+
serverUrl,
|
|
275
|
+
userId,
|
|
276
|
+
record: {
|
|
277
|
+
...existing,
|
|
278
|
+
runtimeBinding: {
|
|
279
|
+
...(existing?.runtimeBinding && typeof existing.runtimeBinding === "object"
|
|
280
|
+
? existing.runtimeBinding
|
|
281
|
+
: {}),
|
|
282
|
+
machineId: machine.machineId,
|
|
283
|
+
ownerUserId: userId,
|
|
284
|
+
},
|
|
285
|
+
updatedAt: Date.now(),
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
let runResponse: any = null;
|
|
290
|
+
const wsTarget = await resolveConnectorWebSocketTarget({
|
|
291
|
+
serverUrl,
|
|
292
|
+
machineId: machine.machineId,
|
|
293
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
294
|
+
fetchImpl,
|
|
295
|
+
});
|
|
296
|
+
await (deps.connectWebSocket ?? defaultConnectWebSocket)(
|
|
297
|
+
wsTarget,
|
|
298
|
+
{
|
|
299
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
300
|
+
sentMessages,
|
|
301
|
+
onMessage: (message) => (deps.connectorRunMessageHandler ?? forwardConnectorRunMessage)(
|
|
302
|
+
machine,
|
|
303
|
+
message,
|
|
304
|
+
(response) => sentMessages.push(response),
|
|
305
|
+
deps.executeCli ?? defaultExecuteCli,
|
|
306
|
+
{
|
|
307
|
+
...env,
|
|
308
|
+
NOLO_SERVER: serverUrl,
|
|
309
|
+
NOLO_SERVER_URL: serverUrl,
|
|
310
|
+
BASE_URL: serverUrl,
|
|
311
|
+
AUTH_TOKEN: authToken,
|
|
312
|
+
NOLO_MACHINE_API_KEY: authToken,
|
|
313
|
+
},
|
|
314
|
+
fetchImpl
|
|
315
|
+
),
|
|
316
|
+
onOpen: async () => {
|
|
317
|
+
const res = await fetchImpl(`${serverUrl}/api/agent/run`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: {
|
|
320
|
+
Authorization: `Bearer ${authToken}`,
|
|
321
|
+
"Content-Type": "application/json",
|
|
322
|
+
},
|
|
323
|
+
body: JSON.stringify({
|
|
324
|
+
agentKey,
|
|
325
|
+
userInput,
|
|
326
|
+
stream: false,
|
|
327
|
+
}),
|
|
328
|
+
});
|
|
329
|
+
runResponse = await res.json().catch(() => ({}));
|
|
330
|
+
if (!res.ok) {
|
|
331
|
+
throw new Error(`agent run failed: HTTP ${res.status} ${JSON.stringify(runResponse)}`);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
output.write(`Smoke OK: ${runResponse?.dialogId ?? "no-dialog"}\n`);
|
|
337
|
+
if (runResponse?.content) output.write(`${runResponse.content}\n`);
|
|
338
|
+
return 0;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
output.write(
|
|
341
|
+
`[nolo] agent smoke-current failed: ${
|
|
342
|
+
error instanceof Error ? error.message : String(error)
|
|
343
|
+
}\n`
|
|
344
|
+
);
|
|
345
|
+
return 1;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export async function runAgentRuntimeDoctorCommand(
|
|
350
|
+
args: string[],
|
|
351
|
+
deps: AgentCommandDeps = {}
|
|
352
|
+
) {
|
|
353
|
+
const env = deps.env ?? process.env;
|
|
354
|
+
const output = deps.output ?? process.stdout;
|
|
355
|
+
const agentInput = args[0]?.trim();
|
|
356
|
+
if (!agentInput || agentInput === "--help" || agentInput === "-h") {
|
|
357
|
+
output.write("Usage: nolo agent runtime-doctor <agentKey>\n");
|
|
358
|
+
return agentInput ? 0 : 1;
|
|
359
|
+
}
|
|
360
|
+
const agentKey = resolveCliAgentKeyInput(agentInput);
|
|
361
|
+
|
|
362
|
+
const authToken = resolveAuthToken(env);
|
|
363
|
+
if (!authToken) {
|
|
364
|
+
output.write("[nolo] agent runtime-doctor requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
|
|
365
|
+
return 1;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const serverUrl = resolveServerUrl(env);
|
|
369
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
370
|
+
const fallbackFetchImpl = deps.fallbackFetchImpl;
|
|
371
|
+
const machine = await (deps.machineInfo ?? detectLaunchableMachineInfo)();
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
const agent = await readAgentRecord({ agentKey, authToken, fallbackFetchImpl, fetchImpl, serverUrl });
|
|
375
|
+
const requiredCapability = requiredCapabilityForAgent(agent);
|
|
376
|
+
const runtimeClass = classifyAgentRuntime(agent);
|
|
377
|
+
const isBoundToCurrent = agent?.runtimeBinding?.machineId === machine.machineId;
|
|
378
|
+
const hasCapability = requiredCapability
|
|
379
|
+
? machine.capabilities.includes(requiredCapability)
|
|
380
|
+
: false;
|
|
381
|
+
output.write(`Agent runtime doctor: ${agent?.name ?? agentKey}\n`);
|
|
382
|
+
if (agentInput !== agentKey) output.write(`Agent input: ${agentInput}\n`);
|
|
383
|
+
output.write(`Agent key: ${agentKey}\n`);
|
|
384
|
+
output.write(`Runtime class: ${runtimeClass}\n`);
|
|
385
|
+
output.write(`API source: ${agent?.apiSource ?? "unknown"}\n`);
|
|
386
|
+
output.write(`CLI provider: ${agent?.cliProvider ?? "none"}\n`);
|
|
387
|
+
output.write(`Required capability: ${requiredCapability || "none"}\n`);
|
|
388
|
+
output.write(`Current machine: ${machine.name} (${machine.machineId})\n`);
|
|
389
|
+
output.write(`Current machine capabilities: ${machine.capabilities.length ? machine.capabilities.join(", ") : "none"}\n`);
|
|
390
|
+
output.write(`Current machine binding: ${runtimeClass === "cli-machine" ? (isBoundToCurrent ? "yes" : "no") : "not required"}\n`);
|
|
391
|
+
output.write(`Current machine has required capability: ${requiredCapability ? (hasCapability ? "yes" : "no") : "not required"}\n`);
|
|
392
|
+
|
|
393
|
+
return shouldRuntimeDoctorPass({ runtimeClass, requiredCapability, hasCapability }) ? 0 : 1;
|
|
394
|
+
} catch (error) {
|
|
395
|
+
output.write(
|
|
396
|
+
`[nolo] agent runtime-doctor failed: ${
|
|
397
|
+
error instanceof Error ? error.message : String(error)
|
|
398
|
+
}\n`
|
|
399
|
+
);
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { resolveCliAgentKeyInput } from "./agentAliases";
|
|
2
|
+
import { getReadableCliDb, type AgentCommandDeps } from "./agentCommandSupport";
|
|
3
|
+
import {
|
|
4
|
+
buildUpdatedAgentRecord,
|
|
5
|
+
normalizeAgentRecordForOutput,
|
|
6
|
+
parseAgentUpdateArgs,
|
|
7
|
+
resolveAgentRecordFromHybridStore,
|
|
8
|
+
writeAgentRecord,
|
|
9
|
+
} from "./agentRecordHelpers";
|
|
10
|
+
import { parseUserIdFromAuthToken, resolveAuthToken } from "./cliEnvHelpers";
|
|
11
|
+
|
|
12
|
+
export async function runAgentReadCommand(
|
|
13
|
+
args: string[],
|
|
14
|
+
deps: AgentCommandDeps = {}
|
|
15
|
+
) {
|
|
16
|
+
const env = deps.env ?? process.env;
|
|
17
|
+
const output = deps.output ?? process.stdout;
|
|
18
|
+
const agentInput = args[0]?.trim();
|
|
19
|
+
if (!agentInput || agentInput === "--help" || agentInput === "-h") {
|
|
20
|
+
output.write("Usage: nolo agent read <agent>\n");
|
|
21
|
+
return agentInput ? 0 : 1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const authToken = resolveAuthToken(env);
|
|
25
|
+
if (!authToken) {
|
|
26
|
+
output.write("[nolo] agent read requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
|
|
27
|
+
return 1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const agentKey = resolveCliAgentKeyInput(agentInput);
|
|
31
|
+
const db = deps.db ?? await getReadableCliDb(output);
|
|
32
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
33
|
+
const fallbackFetchImpl = deps.fallbackFetchImpl;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await resolveAgentRecordFromHybridStore({
|
|
37
|
+
agentInput,
|
|
38
|
+
env,
|
|
39
|
+
db,
|
|
40
|
+
fetchImpl,
|
|
41
|
+
fallbackFetchImpl,
|
|
42
|
+
});
|
|
43
|
+
if (!result) {
|
|
44
|
+
throw new Error(`agent not found: ${agentKey}`);
|
|
45
|
+
}
|
|
46
|
+
output.write(JSON.stringify({
|
|
47
|
+
...normalizeAgentRecordForOutput(result.agentKey, authToken, result.record),
|
|
48
|
+
source: result.source,
|
|
49
|
+
}, null, 2));
|
|
50
|
+
output.write("\n");
|
|
51
|
+
return 0;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
output.write(
|
|
54
|
+
`[nolo] agent read failed: ${
|
|
55
|
+
error instanceof Error ? error.message : String(error)
|
|
56
|
+
}\n`
|
|
57
|
+
);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function runAgentUpdateCommand(
|
|
63
|
+
args: string[],
|
|
64
|
+
deps: AgentCommandDeps = {}
|
|
65
|
+
) {
|
|
66
|
+
const env = deps.env ?? process.env;
|
|
67
|
+
const output = deps.output ?? process.stdout;
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = parseAgentUpdateArgs(args);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
output.write(`[nolo] agent update failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
if (!parsed) {
|
|
76
|
+
output.write(
|
|
77
|
+
"Usage: nolo agent update <agent> [--model <id>] [--cli-provider <provider>] [--api-source <source>] [--prompt <text> | --prompt-file <path> | --prompt-doc <pageKey>] [--tools <json>] [--copy-provider-from <agent>] [--field key=value]\n"
|
|
78
|
+
);
|
|
79
|
+
return args[0] ? 0 : 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const authToken = resolveAuthToken(env);
|
|
83
|
+
if (!authToken) {
|
|
84
|
+
output.write("[nolo] agent update requires an auth token. Run `nolo login` or set AUTH_TOKEN.\n");
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const userId = parseUserIdFromAuthToken(authToken);
|
|
89
|
+
if (!userId) {
|
|
90
|
+
output.write("[nolo] agent update could not read userId from AUTH_TOKEN.\n");
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const db = deps.db ?? await getReadableCliDb(output);
|
|
95
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
96
|
+
const fallbackFetchImpl = deps.fallbackFetchImpl;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const built = await buildUpdatedAgentRecord({
|
|
100
|
+
parsed,
|
|
101
|
+
env,
|
|
102
|
+
db,
|
|
103
|
+
fetchImpl,
|
|
104
|
+
fallbackFetchImpl,
|
|
105
|
+
authToken,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await writeAgentRecord({
|
|
109
|
+
agentKey: built.agentKey,
|
|
110
|
+
authToken,
|
|
111
|
+
fallbackFetchImpl,
|
|
112
|
+
fetchImpl,
|
|
113
|
+
serverUrl: built.serverUrl,
|
|
114
|
+
userId,
|
|
115
|
+
record: built.nextRecord,
|
|
116
|
+
});
|
|
117
|
+
await db.put(built.agentKey, {
|
|
118
|
+
...built.nextRecord,
|
|
119
|
+
dbKey: built.agentKey,
|
|
120
|
+
key: built.nextRecord?.key || built.agentKey,
|
|
121
|
+
serverOrigin: built.serverUrl,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
output.write(JSON.stringify({
|
|
125
|
+
ok: true,
|
|
126
|
+
agentKey: built.agentKey,
|
|
127
|
+
baseUrl: built.serverUrl,
|
|
128
|
+
updates: built.updates,
|
|
129
|
+
record: built.nextRecord,
|
|
130
|
+
}, null, 2));
|
|
131
|
+
output.write("\n");
|
|
132
|
+
return 0;
|
|
133
|
+
} catch (error) {
|
|
134
|
+
output.write(
|
|
135
|
+
`[nolo] agent update failed: ${
|
|
136
|
+
error instanceof Error ? error.message : String(error)
|
|
137
|
+
}\n`
|
|
138
|
+
);
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
}
|