nolo-cli 0.1.19 → 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 +13 -0
- package/agent-runtime/hybridRecordStore.ts +147 -0
- package/agent-runtime/index.ts +69 -0
- package/agent-runtime/localLoop.ts +69 -5
- 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 +1 -0
- package/agent-runtime/workspaceSession.ts +76 -0
- package/agentAliases.ts +37 -0
- package/agentPullCommand.ts +1 -1
- package/agentRunCommand.ts +278 -52
- 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 +621 -9
- package/client/localRuntimeAdapter.ts +275 -250
- 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/machineCommands.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import type { MachineHeartbeat } from "./connector-experimental/protocol";
|
|
2
|
-
import { detectMachineInfo } from "./connector-experimental/machineInfo";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
1
|
+
import type { MachineHeartbeat } from "./connector-experimental/protocol";
|
|
2
|
+
import { detectMachineInfo } from "./connector-experimental/machineInfo";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { mkdirSync, openSync } from "node:fs";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
8
|
+
import {
|
|
9
|
+
collectConnectorRunArtifact,
|
|
10
|
+
readConnectorGitHead,
|
|
11
|
+
resolveConnectorRunCwd,
|
|
12
|
+
} from "./connectorRunArtifact";
|
|
7
13
|
import {
|
|
8
14
|
type HeartbeatLoopOptions,
|
|
9
15
|
runHeartbeatLoop as defaultRunHeartbeatLoop,
|
|
@@ -22,11 +28,11 @@ type ConnectorWebSocketOptions = {
|
|
|
22
28
|
onMessage: (message: string) => void | Promise<void>;
|
|
23
29
|
sentMessages: string[];
|
|
24
30
|
};
|
|
25
|
-
type LocalCliExecutor = (
|
|
26
|
-
provider: string,
|
|
27
|
-
prompt: string,
|
|
28
|
-
options: { model?: string; yolo?: boolean }
|
|
29
|
-
) => Promise<{ text: string; raw?: string; elapsed?: number }>;
|
|
31
|
+
type LocalCliExecutor = (
|
|
32
|
+
provider: string,
|
|
33
|
+
prompt: string,
|
|
34
|
+
options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean; env?: EnvLike }
|
|
35
|
+
) => Promise<{ text: string; raw?: string; elapsed?: number }>;
|
|
30
36
|
|
|
31
37
|
export type MachineSummary = {
|
|
32
38
|
machineId: string;
|
|
@@ -40,12 +46,15 @@ export type MachineSummary = {
|
|
|
40
46
|
lastSeenAt: number;
|
|
41
47
|
};
|
|
42
48
|
|
|
43
|
-
type MachineCommandDeps = {
|
|
44
|
-
env?: EnvLike;
|
|
45
|
-
output?: OutputLike;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
type MachineCommandDeps = {
|
|
50
|
+
env?: EnvLike;
|
|
51
|
+
output?: OutputLike;
|
|
52
|
+
cliEntrypointPath?: string;
|
|
53
|
+
maxConnectorAttempts?: number;
|
|
54
|
+
sleep?: (ms: number) => Promise<void>;
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
machineInfo?: () => MachineHeartbeat;
|
|
57
|
+
runHeartbeatLoop?: (options: HeartbeatLoopOptions) => Promise<void>;
|
|
49
58
|
connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
|
|
50
59
|
executeCli?: LocalCliExecutor;
|
|
51
60
|
spawnDaemon?: (args: {
|
|
@@ -99,13 +108,17 @@ function resolveConnectAuthToken(args: string[], env: EnvLike) {
|
|
|
99
108
|
return readOption(args, "--api-key") || readOption(args, "--token") || resolveAuthToken(env);
|
|
100
109
|
}
|
|
101
110
|
|
|
102
|
-
function resolveDaemonLogPath(env: EnvLike) {
|
|
103
|
-
return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
function resolveDaemonLogPath(env: EnvLike) {
|
|
112
|
+
return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildDaemonCommand(cliEntrypointPath: string | undefined) {
|
|
116
|
+
return [process.execPath, cliEntrypointPath || import.meta.path, "connect", "--ws"];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function defaultSpawnDaemon(args: {
|
|
120
|
+
cmd: string[];
|
|
121
|
+
cwd: string;
|
|
109
122
|
env: EnvLike;
|
|
110
123
|
logPath: string;
|
|
111
124
|
}) {
|
|
@@ -114,24 +127,35 @@ function defaultSpawnDaemon(args: {
|
|
|
114
127
|
const env = Object.fromEntries(
|
|
115
128
|
Object.entries(args.env).filter((entry): entry is [string, string] => typeof entry[1] === "string")
|
|
116
129
|
);
|
|
117
|
-
const proc =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
const proc = spawn(args.cmd[0], args.cmd.slice(1), {
|
|
131
|
+
cwd: args.cwd,
|
|
132
|
+
env,
|
|
133
|
+
detached: true,
|
|
134
|
+
stdio: ["ignore", out, out],
|
|
135
|
+
});
|
|
136
|
+
proc.unref();
|
|
137
|
+
return { pid: proc.pid };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveHeartbeatIntervalMs(env: EnvLike) {
|
|
141
|
+
const raw = Number(env.NOLO_CONNECT_HEARTBEAT_MS ?? "");
|
|
142
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 30_000;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const CONNECTOR_WS_KEEPALIVE_MS = 25_000;
|
|
146
|
+
|
|
147
|
+
function buildConnectorKeepaliveMessage() {
|
|
148
|
+
return JSON.stringify({ type: "connector.keepalive", sentAt: Date.now() });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const defaultSleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
152
|
+
|
|
153
|
+
function resolveConnectorReconnectDelayMs(env: EnvLike) {
|
|
154
|
+
const raw = Number(env.NOLO_CONNECT_RECONNECT_MS ?? "");
|
|
155
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : 5_000;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function defaultConnectWebSocket(url: string, options: ConnectorWebSocketOptions) {
|
|
135
159
|
const WebSocketCtor = globalThis.WebSocket;
|
|
136
160
|
if (!WebSocketCtor) {
|
|
137
161
|
throw new Error("WebSocket is not available in this runtime");
|
|
@@ -139,16 +163,34 @@ async function defaultConnectWebSocket(url: string, options: ConnectorWebSocketO
|
|
|
139
163
|
await new Promise<void>((resolve, reject) => {
|
|
140
164
|
const ws = new WebSocketCtor(url, {
|
|
141
165
|
headers: options.headers,
|
|
142
|
-
} as any);
|
|
143
|
-
let opened = false;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
166
|
+
} as any);
|
|
167
|
+
let opened = false;
|
|
168
|
+
let keepalive: ReturnType<typeof setInterval> | null = null;
|
|
169
|
+
const clearKeepalive = () => {
|
|
170
|
+
if (keepalive) {
|
|
171
|
+
clearInterval(keepalive);
|
|
172
|
+
keepalive = null;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
ws.addEventListener("open", () => {
|
|
176
|
+
opened = true;
|
|
177
|
+
keepalive = setInterval(() => {
|
|
178
|
+
try {
|
|
179
|
+
ws.send(buildConnectorKeepaliveMessage());
|
|
180
|
+
} catch {
|
|
181
|
+
clearKeepalive();
|
|
182
|
+
}
|
|
183
|
+
}, CONNECTOR_WS_KEEPALIVE_MS);
|
|
184
|
+
}, { once: true });
|
|
185
|
+
ws.addEventListener("error", () => {
|
|
186
|
+
clearKeepalive();
|
|
187
|
+
reject(new Error("connector websocket failed"));
|
|
188
|
+
});
|
|
189
|
+
ws.addEventListener("close", () => {
|
|
190
|
+
clearKeepalive();
|
|
191
|
+
if (opened) resolve();
|
|
192
|
+
else reject(new Error("connector websocket closed before opening"));
|
|
193
|
+
}, { once: true });
|
|
152
194
|
ws.addEventListener("message", (event) => {
|
|
153
195
|
const startIndex = options.sentMessages.length;
|
|
154
196
|
Promise.resolve(options.onMessage(String(event.data))).then(() => {
|
|
@@ -160,30 +202,96 @@ async function defaultConnectWebSocket(url: string, options: ConnectorWebSocketO
|
|
|
160
202
|
});
|
|
161
203
|
}
|
|
162
204
|
|
|
163
|
-
async function defaultExecuteCli(
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
205
|
+
async function defaultExecuteCli(
|
|
206
|
+
provider: string,
|
|
207
|
+
prompt: string,
|
|
208
|
+
options: { model?: string; timeout?: number; cwd?: string; yolo?: boolean; env?: EnvLike }
|
|
209
|
+
) {
|
|
210
|
+
const { executeCli } = await import("./ai/agent/cliExecutor");
|
|
211
|
+
return executeCli(provider as any, prompt, options);
|
|
212
|
+
}
|
|
167
213
|
|
|
168
214
|
function detectLaunchableMachineInfo() {
|
|
169
215
|
return detectMachineInfo({ probeLaunchable: true });
|
|
170
216
|
}
|
|
171
217
|
|
|
172
|
-
function
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
218
|
+
function inferTaskRunRole(agentKey: string, agentConfig: any) {
|
|
219
|
+
const haystack = [
|
|
220
|
+
agentKey,
|
|
221
|
+
agentConfig?.name,
|
|
222
|
+
agentConfig?.prompt,
|
|
223
|
+
].filter(Boolean).join("\n").toLowerCase();
|
|
224
|
+
if (haystack.includes("frontend") || haystack.includes("前端")) return "frontend";
|
|
225
|
+
if (haystack.includes("fullstack") || haystack.includes("全栈")) return "fullstack";
|
|
226
|
+
if (haystack.includes("review") || haystack.includes("审查")) return "reviewer";
|
|
227
|
+
if (haystack.includes("project-manager") || haystack.includes("项目经理")) return "pm";
|
|
228
|
+
return "codex";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildTaskRunBridgePrompt(args: {
|
|
232
|
+
agentKey: string;
|
|
233
|
+
agentConfig: any;
|
|
234
|
+
runtimeContext: any;
|
|
235
|
+
}) {
|
|
236
|
+
const taskRun = args.runtimeContext?.taskRun;
|
|
237
|
+
const rowDbKey = typeof taskRun?.rowDbKey === "string" ? taskRun.rowDbKey.trim() : "";
|
|
238
|
+
if (!rowDbKey) return "";
|
|
239
|
+
const workItemId = typeof taskRun?.workItemId === "string" ? taskRun.workItemId.trim() : "";
|
|
240
|
+
const role = inferTaskRunRole(args.agentKey, args.agentConfig);
|
|
241
|
+
return [
|
|
242
|
+
"--- Nolo task-run tool bridge ---",
|
|
243
|
+
"This CLI runtime does not receive server-side function tools directly.",
|
|
244
|
+
"Use the local task-run bridge command for durable task state writes. Do not edit meta.taskRun with table or database commands.",
|
|
245
|
+
"Run commands from the repository root. The runner already provides server URL and auth in the environment.",
|
|
246
|
+
"Release boundary: after review passes, AI/reviewer/Codex may advance alpha for verification. Do not merge, push, or release main/release unless the human owner explicitly authorizes it in the current task context.",
|
|
247
|
+
"Cost boundary: read taskRun.control from context. Prefer low-cost/free-window models for routine work, and reserve Codex for supervision, quality gates, complex unblocking, and main/release decisions.",
|
|
248
|
+
`Read context: bun packages/cli/index.ts task-run read-context --row-dbkey ${JSON.stringify(rowDbKey)}`,
|
|
249
|
+
...(workItemId
|
|
250
|
+
? [
|
|
251
|
+
`Claim work item: bun packages/cli/index.ts task-run claim-work-item --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --agent-key ${JSON.stringify(args.agentKey)} --role ${role}`,
|
|
252
|
+
`Set blocker: bun packages/cli/index.ts task-run set-blocker --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --layer tool --code <CODE> --message <MESSAGE>`,
|
|
253
|
+
`Clear resolved blocker: bun packages/cli/index.ts task-run clear-blocker --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --blocker-id <BLOCKER_ID>`,
|
|
254
|
+
`Submit outcome: bun packages/cli/index.ts task-run submit-outcome --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --summary <SUMMARY>`,
|
|
255
|
+
`Request review: bun packages/cli/index.ts task-run request-review --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --reviewer-agent-key <AGENT_KEY> --message <MESSAGE>`,
|
|
256
|
+
`Record review: bun packages/cli/index.ts task-run record-review --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --review-status <passed|needs_changes|blocked> --reviewer-agent-key ${JSON.stringify(args.agentKey)} --findings <FINDINGS>`,
|
|
257
|
+
`Close work item: bun packages/cli/index.ts task-run close-work-item --row-dbkey ${JSON.stringify(rowDbKey)} --work-item-id ${JSON.stringify(workItemId)} --summary <SUMMARY>`,
|
|
258
|
+
]
|
|
259
|
+
: []),
|
|
260
|
+
"If a bridge command fails, report the exact command and error.",
|
|
261
|
+
].join("\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function buildConnectorCliPrompt(agentConfig: any, userInput: string, bridgeArgs?: {
|
|
265
|
+
agentKey: string;
|
|
266
|
+
runtimeContext: any;
|
|
267
|
+
}) {
|
|
268
|
+
const policy = resolveMachineRunPermissionPolicy(agentConfig);
|
|
269
|
+
return [
|
|
270
|
+
typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
|
|
271
|
+
bridgeArgs ? buildTaskRunBridgePrompt({
|
|
272
|
+
agentKey: bridgeArgs.agentKey,
|
|
273
|
+
agentConfig,
|
|
274
|
+
runtimeContext: bridgeArgs.runtimeContext,
|
|
275
|
+
}) : "",
|
|
276
|
+
buildMachinePermissionPromptBlock(policy),
|
|
277
|
+
`--- User task ---\n${userInput}`,
|
|
278
|
+
].filter(Boolean).join("\n\n");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function normalizeConnectorRunTimeoutMs(value: unknown): number | undefined {
|
|
282
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
return Math.floor(value);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function handleConnectorRunMessage(
|
|
289
|
+
machine: MachineHeartbeat,
|
|
290
|
+
message: string,
|
|
291
|
+
send: (message: string) => void,
|
|
292
|
+
executeCli: LocalCliExecutor,
|
|
293
|
+
runtimeEnv: EnvLike
|
|
294
|
+
) {
|
|
187
295
|
let parsed: any;
|
|
188
296
|
try {
|
|
189
297
|
parsed = JSON.parse(message);
|
|
@@ -196,37 +304,124 @@ async function handleConnectorRunMessage(
|
|
|
196
304
|
if (agentConfig.apiSource !== "cli") {
|
|
197
305
|
throw new Error("Connector can only execute CLI agents. Set the agent apiSource to cli and choose a cliProvider.");
|
|
198
306
|
}
|
|
199
|
-
const provider = String(agentConfig.cliProvider || "copilot");
|
|
200
|
-
const policy = resolveMachineRunPermissionPolicy(agentConfig);
|
|
201
|
-
const userInput = String(parsed.payload?.userInput ?? "");
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
307
|
+
const provider = String(agentConfig.cliProvider || "copilot");
|
|
308
|
+
const policy = resolveMachineRunPermissionPolicy(agentConfig);
|
|
309
|
+
const userInput = String(parsed.payload?.userInput ?? "");
|
|
310
|
+
const timeout = normalizeConnectorRunTimeoutMs(parsed.payload?.timeoutMs);
|
|
311
|
+
assertMachineRunAllowed(userInput, policy);
|
|
312
|
+
const cwd = resolveConnectorRunCwd({ env: runtimeEnv, policy });
|
|
313
|
+
const baseSha = await readConnectorGitHead(cwd);
|
|
314
|
+
const result = await executeCli(
|
|
315
|
+
provider,
|
|
316
|
+
buildConnectorCliPrompt(agentConfig, userInput, {
|
|
317
|
+
agentKey: String(parsed.payload?.agentKey ?? ""),
|
|
318
|
+
runtimeContext: parsed.payload?.runtimeContext,
|
|
319
|
+
}),
|
|
320
|
+
{
|
|
321
|
+
model: agentConfig.model || undefined,
|
|
322
|
+
timeout,
|
|
323
|
+
cwd,
|
|
324
|
+
yolo: true,
|
|
325
|
+
env: {
|
|
326
|
+
NOLO_SERVER: runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL || runtimeEnv.BASE_URL,
|
|
327
|
+
NOLO_SERVER_URL: runtimeEnv.NOLO_SERVER_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL,
|
|
328
|
+
BASE_URL: runtimeEnv.BASE_URL || runtimeEnv.NOLO_SERVER || runtimeEnv.NOLO_SERVER_URL,
|
|
329
|
+
AUTH_TOKEN: runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH || runtimeEnv.NOLO_MACHINE_API_KEY,
|
|
330
|
+
NOLO_MACHINE_API_KEY: runtimeEnv.NOLO_MACHINE_API_KEY || runtimeEnv.AUTH_TOKEN || runtimeEnv.AUTH,
|
|
331
|
+
},
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
const artifacts = await collectConnectorRunArtifact({
|
|
335
|
+
cwd,
|
|
336
|
+
baseSha,
|
|
337
|
+
exitStatus: "completed",
|
|
338
|
+
});
|
|
339
|
+
send(JSON.stringify({
|
|
212
340
|
type: "agent.run.result",
|
|
213
341
|
requestId: parsed.requestId,
|
|
214
342
|
result: {
|
|
215
|
-
content: result.text,
|
|
216
|
-
model: agentConfig.model ?? provider,
|
|
217
|
-
trace: [{ role: "assistant", content: result.text }],
|
|
218
|
-
|
|
219
|
-
|
|
343
|
+
content: result.text,
|
|
344
|
+
model: agentConfig.model ?? provider,
|
|
345
|
+
trace: [{ role: "assistant", content: result.text }],
|
|
346
|
+
artifacts,
|
|
347
|
+
},
|
|
348
|
+
}));
|
|
220
349
|
} catch (error) {
|
|
221
350
|
send(JSON.stringify({
|
|
222
351
|
type: "agent.run.result",
|
|
223
352
|
requestId: parsed.requestId,
|
|
224
353
|
error: error instanceof Error ? error.message : String(error),
|
|
225
354
|
}));
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function runConnectorWebSocketSession(options: {
|
|
359
|
+
env: EnvLike;
|
|
360
|
+
output: OutputLike;
|
|
361
|
+
machine: MachineHeartbeat;
|
|
362
|
+
sendHeartbeat: () => Promise<void>;
|
|
363
|
+
runHeartbeatLoop: (options: HeartbeatLoopOptions) => Promise<void>;
|
|
364
|
+
connectWebSocket: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
|
|
365
|
+
executeCli: LocalCliExecutor;
|
|
366
|
+
fetchImpl: typeof fetch;
|
|
367
|
+
serverUrl: string;
|
|
368
|
+
authToken: string;
|
|
369
|
+
}) {
|
|
370
|
+
const sentMessages: string[] = [];
|
|
371
|
+
const heartbeatAbort = new AbortController();
|
|
372
|
+
try {
|
|
373
|
+
await options.sendHeartbeat();
|
|
374
|
+
options.output.write(
|
|
375
|
+
`Connector websocket connected: ${options.machine.name} (${options.machine.machineId})\n`
|
|
376
|
+
);
|
|
377
|
+
const heartbeatLoopPromise = options.runHeartbeatLoop({
|
|
378
|
+
intervalMs: resolveHeartbeatIntervalMs(options.env),
|
|
379
|
+
sendHeartbeat: options.sendHeartbeat,
|
|
380
|
+
signal: heartbeatAbort.signal,
|
|
381
|
+
});
|
|
382
|
+
const wsTarget = await resolveConnectorWebSocketTarget({
|
|
383
|
+
serverUrl: options.serverUrl,
|
|
384
|
+
machineId: options.machine.machineId,
|
|
385
|
+
headers: { Authorization: `Bearer ${options.authToken}` },
|
|
386
|
+
fetchImpl: options.fetchImpl,
|
|
387
|
+
});
|
|
388
|
+
const websocketPromise = options.connectWebSocket(
|
|
389
|
+
wsTarget,
|
|
390
|
+
{
|
|
391
|
+
headers: { Authorization: `Bearer ${options.authToken}` },
|
|
392
|
+
sentMessages,
|
|
393
|
+
onMessage: (message) => handleConnectorRunMessage(
|
|
394
|
+
options.machine,
|
|
395
|
+
message,
|
|
396
|
+
(response) => sentMessages.push(response),
|
|
397
|
+
options.executeCli,
|
|
398
|
+
{
|
|
399
|
+
...options.env,
|
|
400
|
+
NOLO_SERVER: options.serverUrl,
|
|
401
|
+
NOLO_SERVER_URL: options.serverUrl,
|
|
402
|
+
BASE_URL: options.serverUrl,
|
|
403
|
+
AUTH_TOKEN: options.authToken,
|
|
404
|
+
NOLO_MACHINE_API_KEY: options.authToken,
|
|
405
|
+
}
|
|
406
|
+
),
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
await Promise.race([websocketPromise, heartbeatLoopPromise]);
|
|
410
|
+
heartbeatAbort.abort();
|
|
411
|
+
await Promise.allSettled([websocketPromise, heartbeatLoopPromise]);
|
|
412
|
+
return 0;
|
|
413
|
+
} catch (error) {
|
|
414
|
+
heartbeatAbort.abort();
|
|
415
|
+
options.output.write(
|
|
416
|
+
`[nolo] Connector websocket failed: ${
|
|
417
|
+
error instanceof Error ? error.message : String(error)
|
|
418
|
+
}\n`
|
|
419
|
+
);
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function formatMachineStatus(machines: MachineSummary[]) {
|
|
230
425
|
if (machines.length === 0) {
|
|
231
426
|
return "No connected machines.\nRun `nolo connect` on this computer to register it once.\n";
|
|
232
427
|
}
|
|
@@ -273,62 +468,41 @@ export async function runMachineConnectCommand(
|
|
|
273
468
|
}
|
|
274
469
|
};
|
|
275
470
|
|
|
276
|
-
if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
|
|
277
|
-
const logPath = resolveDaemonLogPath(env);
|
|
278
|
-
const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
|
|
279
|
-
cmd:
|
|
280
|
-
cwd: process.cwd(),
|
|
281
|
-
env,
|
|
282
|
-
logPath,
|
|
471
|
+
if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
|
|
472
|
+
const logPath = resolveDaemonLogPath(env);
|
|
473
|
+
const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
|
|
474
|
+
cmd: buildDaemonCommand(deps.cliEntrypointPath),
|
|
475
|
+
cwd: process.cwd(),
|
|
476
|
+
env,
|
|
477
|
+
logPath,
|
|
283
478
|
});
|
|
284
479
|
output.write(`Connector daemon started${result.pid ? ` pid=${result.pid}` : ""}. Log: ${logPath}\n`);
|
|
285
480
|
return 0;
|
|
286
481
|
}
|
|
287
482
|
|
|
288
|
-
if (hasFlag(args, "--ws")) {
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
machine,
|
|
312
|
-
message,
|
|
313
|
-
(response) => sentMessages.push(response),
|
|
314
|
-
deps.executeCli ?? defaultExecuteCli
|
|
315
|
-
),
|
|
316
|
-
}
|
|
317
|
-
);
|
|
318
|
-
await Promise.race([websocketPromise, heartbeatLoopPromise]);
|
|
319
|
-
heartbeatAbort.abort();
|
|
320
|
-
await Promise.allSettled([websocketPromise, heartbeatLoopPromise]);
|
|
321
|
-
return 0;
|
|
322
|
-
} catch (error) {
|
|
323
|
-
heartbeatAbort.abort();
|
|
324
|
-
output.write(
|
|
325
|
-
`[nolo] Connector websocket failed: ${
|
|
326
|
-
error instanceof Error ? error.message : String(error)
|
|
327
|
-
}\n`
|
|
328
|
-
);
|
|
329
|
-
return 1;
|
|
330
|
-
}
|
|
331
|
-
}
|
|
483
|
+
if (hasFlag(args, "--ws")) {
|
|
484
|
+
const maxAttempts = deps.maxConnectorAttempts ?? Infinity;
|
|
485
|
+
const sleep = deps.sleep ?? defaultSleep;
|
|
486
|
+
const reconnectDelayMs = resolveConnectorReconnectDelayMs(env);
|
|
487
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
488
|
+
const exitCode = await runConnectorWebSocketSession({
|
|
489
|
+
env,
|
|
490
|
+
output,
|
|
491
|
+
machine,
|
|
492
|
+
sendHeartbeat,
|
|
493
|
+
runHeartbeatLoop: deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop,
|
|
494
|
+
connectWebSocket: deps.connectWebSocket ?? defaultConnectWebSocket,
|
|
495
|
+
executeCli: deps.executeCli ?? defaultExecuteCli,
|
|
496
|
+
fetchImpl,
|
|
497
|
+
serverUrl,
|
|
498
|
+
authToken,
|
|
499
|
+
});
|
|
500
|
+
if (attempt >= maxAttempts) return exitCode;
|
|
501
|
+
output.write(`[nolo] Connector websocket disconnected. Reconnecting in ${reconnectDelayMs}ms.\n`);
|
|
502
|
+
await sleep(reconnectDelayMs);
|
|
503
|
+
}
|
|
504
|
+
return 0;
|
|
505
|
+
}
|
|
332
506
|
|
|
333
507
|
if (hasFlag(args, "--watch")) {
|
|
334
508
|
output.write(`Connecting machine heartbeat loop: ${machine.name} (${machine.platform}/${machine.arch})\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nolo-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first terminal workspace for Nolo",
|
|
6
6
|
"bin": {
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
"agentRunCommand.ts",
|
|
18
18
|
"authCommands.ts",
|
|
19
19
|
"commandRegistry.ts",
|
|
20
|
+
"connectorRunArtifact.ts",
|
|
20
21
|
"connectorWebSocketTarget.ts",
|
|
21
22
|
"defaultServer.ts",
|
|
22
23
|
"machineCommands.ts",
|
|
24
|
+
"taskRunCommand.ts",
|
|
23
25
|
"runtimeModeArgs.ts",
|
|
26
|
+
"tableCommands.ts",
|
|
24
27
|
"updateCommands.ts",
|
|
25
28
|
"client/**/*.ts",
|
|
26
29
|
"tui/**/*.ts",
|