nolo-cli 0.1.6 → 0.1.8
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 +75 -4
- package/agentRuntimeCommands.ts +464 -0
- package/client/agentRun.ts +198 -167
- package/commandRegistry.ts +14 -0
- package/connectorWebSocketTarget.ts +29 -0
- package/defaultServer.ts +1 -0
- package/index.ts +158 -104
- package/machineCommands.ts +382 -0
- package/package.json +9 -1
- package/tui/readlineWorkspace.ts +50 -0
- package/tui/session.ts +64 -2
- package/updateCommands.ts +70 -5
package/index.ts
CHANGED
|
@@ -1,105 +1,159 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
import { renderHelpText, resolveCommand } from "./commandRegistry";
|
|
7
|
-
import { runLoginCommand, runLogoutCommand, runWhoamiCommand } from "./authCommands";
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
process.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
2
|
+
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { renderHelpText, resolveCommand } from "./commandRegistry";
|
|
7
|
+
import { runLoginCommand, runLogoutCommand, runWhoamiCommand } from "./authCommands";
|
|
8
|
+
import {
|
|
9
|
+
runAgentBindCurrentCommand,
|
|
10
|
+
runAgentRuntimeDoctorCommand,
|
|
11
|
+
runAgentSmokeCurrentCommand,
|
|
12
|
+
} from "./agentRuntimeCommands";
|
|
13
|
+
import { buildEnvFromProfile, loadProfileConfig } from "./client/profileConfig";
|
|
14
|
+
import {
|
|
15
|
+
runMachineConnectCommand,
|
|
16
|
+
runMachineStatusCommand,
|
|
17
|
+
} from "./machineCommands";
|
|
18
|
+
import { startTuiWorkspace } from "./tui/readlineWorkspace";
|
|
19
|
+
import {
|
|
20
|
+
buildCliDoctorText,
|
|
21
|
+
buildCliVersionText,
|
|
22
|
+
readPackageInfo,
|
|
23
|
+
runSelfUpdate,
|
|
24
|
+
} from "./updateCommands";
|
|
25
|
+
import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
26
|
+
|
|
27
|
+
const CLI_DIR = dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const ROOT_DIR = join(CLI_DIR, "..", "..");
|
|
29
|
+
const SCRIPT_DIR = join(ROOT_DIR, "scripts");
|
|
30
|
+
const packageInfo = readPackageInfo();
|
|
31
|
+
|
|
32
|
+
async function runScript(script: string, forwardedArgs: string[], env: NodeJS.ProcessEnv) {
|
|
33
|
+
const scriptPath = join(SCRIPT_DIR, script);
|
|
34
|
+
const proc = Bun.spawn({
|
|
35
|
+
cmd: [process.execPath, scriptPath, ...forwardedArgs],
|
|
36
|
+
stdin: "inherit",
|
|
37
|
+
stdout: "inherit",
|
|
38
|
+
stderr: "inherit",
|
|
39
|
+
env,
|
|
40
|
+
});
|
|
41
|
+
const exitCode = await proc.exited;
|
|
42
|
+
process.exit(exitCode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const args = process.argv.slice(2);
|
|
46
|
+
const profileEnv = buildEnvFromProfile(loadProfileConfig());
|
|
47
|
+
const runtimeEnv = {
|
|
48
|
+
...profileEnv,
|
|
49
|
+
...process.env,
|
|
50
|
+
NOLO_CLI_VERSION: packageInfo.version,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function readOption(args: string[], flag: string) {
|
|
54
|
+
const index = args.indexOf(flag);
|
|
55
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildDaemonEnv(args: string[]) {
|
|
59
|
+
const serverUrl = readOption(args, "--server-url") || readOption(args, "--server");
|
|
60
|
+
const apiKey = readOption(args, "--api-key") || readOption(args, "--token");
|
|
61
|
+
return {
|
|
62
|
+
...runtimeEnv,
|
|
63
|
+
...(serverUrl ? { NOLO_SERVER: serverUrl } : {}),
|
|
64
|
+
...(apiKey ? { AUTH_TOKEN: apiKey } : {}),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function looksLikeDaemonShortcut(args: string[]) {
|
|
69
|
+
return args.includes("--server-url") || args.includes("--api-key");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (args.length === 0) {
|
|
73
|
+
if (process.stdin.isTTY) {
|
|
74
|
+
await startTuiWorkspace({ scriptDir: SCRIPT_DIR, env: runtimeEnv });
|
|
75
|
+
} else {
|
|
76
|
+
console.log(renderHelpText());
|
|
77
|
+
}
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (args.length === 1 && (args[0] === "tui" || args[0] === "chat")) {
|
|
82
|
+
await startTuiWorkspace({ scriptDir: SCRIPT_DIR, env: runtimeEnv });
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (args[0] === "login") {
|
|
87
|
+
process.exit(await runLoginCommand(args.slice(1)));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (args[0] === "whoami") {
|
|
91
|
+
process.exit(runWhoamiCommand());
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (args[0] === "logout") {
|
|
95
|
+
process.exit(runLogoutCommand());
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (args[0] === "connect") {
|
|
99
|
+
process.exit(await runMachineConnectCommand(args.slice(1), { env: runtimeEnv }));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (args[0] === "daemon" || looksLikeDaemonShortcut(args)) {
|
|
103
|
+
const daemonArgs = args[0] === "daemon" ? args.slice(1) : args;
|
|
104
|
+
process.exit(await runMachineConnectCommand(["--ws"], { env: buildDaemonEnv(daemonArgs) }));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (args[0] === "machine" && args[1] === "status") {
|
|
108
|
+
process.exit(await runMachineStatusCommand(args.slice(2), { env: runtimeEnv }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (args[0] === "agent" && args[1] === "bind-current") {
|
|
112
|
+
process.exit(await runAgentBindCurrentCommand(args.slice(2), { env: runtimeEnv }));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (args[0] === "agent" && args[1] === "smoke-current") {
|
|
116
|
+
process.exit(await runAgentSmokeCurrentCommand(args.slice(2), { env: runtimeEnv }));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (args[0] === "agent" && args[1] === "runtime-doctor") {
|
|
120
|
+
process.exit(await runAgentRuntimeDoctorCommand(args.slice(2), { env: runtimeEnv }));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (args[0] === "--version" || args[0] === "-v" || args[0] === "version") {
|
|
124
|
+
console.log(buildCliVersionText(packageInfo));
|
|
125
|
+
process.exit(0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (args[0] === "doctor") {
|
|
129
|
+
console.log(
|
|
130
|
+
buildCliDoctorText({
|
|
131
|
+
packageName: packageInfo.name,
|
|
132
|
+
version: packageInfo.version,
|
|
133
|
+
entrypoint: fileURLToPath(import.meta.url),
|
|
134
|
+
serverUrl: runtimeEnv.NOLO_SERVER || runtimeEnv.BASE_URL || DEFAULT_NOLO_SERVER_URL,
|
|
135
|
+
profileName: runtimeEnv.NOLO_PROFILE || "local",
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
process.exit(0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (args[0] === "update") {
|
|
142
|
+
process.exit(await runSelfUpdate());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
146
|
+
console.log(renderHelpText());
|
|
147
|
+
process.exit(0);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const command = resolveCommand(args);
|
|
151
|
+
if (!command) {
|
|
152
|
+
console.error(`Unknown command: ${args.join(" ")}`);
|
|
153
|
+
console.error("");
|
|
154
|
+
console.log(renderHelpText());
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const forwardedArgs = args.slice(command.path.length);
|
|
159
|
+
await runScript(command.script, forwardedArgs, runtimeEnv);
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import type { MachineHeartbeat } from "connector-experimental/protocol";
|
|
2
|
+
import { detectMachineInfo } from "connector-experimental/machineInfo";
|
|
3
|
+
import { mkdirSync, openSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { DEFAULT_NOLO_SERVER_URL } from "./defaultServer";
|
|
7
|
+
import {
|
|
8
|
+
type HeartbeatLoopOptions,
|
|
9
|
+
runHeartbeatLoop as defaultRunHeartbeatLoop,
|
|
10
|
+
} from "connector-experimental/heartbeatLoop";
|
|
11
|
+
import {
|
|
12
|
+
assertMachineRunAllowed,
|
|
13
|
+
buildMachinePermissionPromptBlock,
|
|
14
|
+
resolveMachineRunPermissionPolicy,
|
|
15
|
+
} from "../ai/agent/machineRunPermissions";
|
|
16
|
+
import { resolveConnectorWebSocketTarget } from "./connectorWebSocketTarget";
|
|
17
|
+
|
|
18
|
+
type EnvLike = Record<string, string | undefined>;
|
|
19
|
+
type OutputLike = { write(chunk: string): unknown };
|
|
20
|
+
type ConnectorWebSocketOptions = {
|
|
21
|
+
headers: Record<string, string>;
|
|
22
|
+
onMessage: (message: string) => void | Promise<void>;
|
|
23
|
+
sentMessages: string[];
|
|
24
|
+
};
|
|
25
|
+
type LocalCliExecutor = (
|
|
26
|
+
provider: string,
|
|
27
|
+
prompt: string,
|
|
28
|
+
options: { model?: string; yolo?: boolean }
|
|
29
|
+
) => Promise<{ text: string; raw?: string; elapsed?: number }>;
|
|
30
|
+
|
|
31
|
+
export type MachineSummary = {
|
|
32
|
+
machineId: string;
|
|
33
|
+
name: string;
|
|
34
|
+
platform: string;
|
|
35
|
+
arch: string;
|
|
36
|
+
connectorVersion: string | null;
|
|
37
|
+
capabilities: string[];
|
|
38
|
+
connectorStatus?: "connected" | "disconnected";
|
|
39
|
+
status: "online" | "offline";
|
|
40
|
+
lastSeenAt: number;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type MachineCommandDeps = {
|
|
44
|
+
env?: EnvLike;
|
|
45
|
+
output?: OutputLike;
|
|
46
|
+
fetchImpl?: typeof fetch;
|
|
47
|
+
machineInfo?: () => MachineHeartbeat;
|
|
48
|
+
runHeartbeatLoop?: (options: HeartbeatLoopOptions) => Promise<void>;
|
|
49
|
+
connectWebSocket?: (url: string, options: ConnectorWebSocketOptions) => Promise<void>;
|
|
50
|
+
executeCli?: LocalCliExecutor;
|
|
51
|
+
spawnDaemon?: (args: {
|
|
52
|
+
cmd: string[];
|
|
53
|
+
cwd: string;
|
|
54
|
+
env: EnvLike;
|
|
55
|
+
logPath: string;
|
|
56
|
+
}) => { pid?: number };
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function resolveServerUrl(env: EnvLike) {
|
|
60
|
+
return (env.NOLO_SERVER || env.BASE_URL || DEFAULT_NOLO_SERVER_URL).replace(/\/+$/, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function resolveAuthToken(env: EnvLike) {
|
|
64
|
+
return env.AUTH_TOKEN || env.AUTH || "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function writeAuthMissing(output: OutputLike) {
|
|
68
|
+
output.write(
|
|
69
|
+
"[nolo] Machine commands require an auth token. Run `nolo login` or set AUTH_TOKEN.\n"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hasFlag(args: string[], flag: string) {
|
|
74
|
+
return args.includes(flag);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function resolveDaemonLogPath(env: EnvLike) {
|
|
78
|
+
return env.NOLO_CONNECT_LOG || join(homedir(), ".nolo", "logs", "connector.log");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function defaultSpawnDaemon(args: {
|
|
82
|
+
cmd: string[];
|
|
83
|
+
cwd: string;
|
|
84
|
+
env: EnvLike;
|
|
85
|
+
logPath: string;
|
|
86
|
+
}) {
|
|
87
|
+
mkdirSync(dirname(args.logPath), { recursive: true });
|
|
88
|
+
const out = openSync(args.logPath, "a");
|
|
89
|
+
const env = Object.fromEntries(
|
|
90
|
+
Object.entries(args.env).filter((entry): entry is [string, string] => typeof entry[1] === "string")
|
|
91
|
+
);
|
|
92
|
+
const proc = Bun.spawn({
|
|
93
|
+
cmd: args.cmd,
|
|
94
|
+
cwd: args.cwd,
|
|
95
|
+
env,
|
|
96
|
+
stdin: "ignore",
|
|
97
|
+
stdout: out,
|
|
98
|
+
stderr: out,
|
|
99
|
+
});
|
|
100
|
+
proc.unref();
|
|
101
|
+
return { pid: proc.pid };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveHeartbeatIntervalMs(env: EnvLike) {
|
|
105
|
+
const raw = Number(env.NOLO_CONNECT_HEARTBEAT_MS ?? "");
|
|
106
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 30_000;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function defaultConnectWebSocket(url: string, options: ConnectorWebSocketOptions) {
|
|
110
|
+
const WebSocketCtor = globalThis.WebSocket;
|
|
111
|
+
if (!WebSocketCtor) {
|
|
112
|
+
throw new Error("WebSocket is not available in this runtime");
|
|
113
|
+
}
|
|
114
|
+
await new Promise<void>((resolve, reject) => {
|
|
115
|
+
const ws = new WebSocketCtor(url, {
|
|
116
|
+
headers: options.headers,
|
|
117
|
+
} as any);
|
|
118
|
+
let opened = false;
|
|
119
|
+
ws.addEventListener("open", () => {
|
|
120
|
+
opened = true;
|
|
121
|
+
}, { once: true });
|
|
122
|
+
ws.addEventListener("error", () => reject(new Error("connector websocket failed")));
|
|
123
|
+
ws.addEventListener("close", () => {
|
|
124
|
+
if (opened) resolve();
|
|
125
|
+
else reject(new Error("connector websocket closed before opening"));
|
|
126
|
+
}, { once: true });
|
|
127
|
+
ws.addEventListener("message", (event) => {
|
|
128
|
+
const startIndex = options.sentMessages.length;
|
|
129
|
+
Promise.resolve(options.onMessage(String(event.data))).then(() => {
|
|
130
|
+
for (const message of options.sentMessages.slice(startIndex)) {
|
|
131
|
+
ws.send(message);
|
|
132
|
+
}
|
|
133
|
+
}).catch(() => undefined);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function defaultExecuteCli(provider: string, prompt: string, options: { model?: string; yolo?: boolean }) {
|
|
139
|
+
const { executeCli } = await import("ai/agent/cliExecutor");
|
|
140
|
+
return executeCli(provider as any, prompt, options);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function detectLaunchableMachineInfo() {
|
|
144
|
+
return detectMachineInfo({ probeLaunchable: true });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildConnectorCliPrompt(agentConfig: any, userInput: string) {
|
|
148
|
+
const policy = resolveMachineRunPermissionPolicy(agentConfig);
|
|
149
|
+
return [
|
|
150
|
+
typeof agentConfig?.prompt === "string" ? agentConfig.prompt.trim() : "",
|
|
151
|
+
buildMachinePermissionPromptBlock(policy),
|
|
152
|
+
`--- User task ---\n${userInput}`,
|
|
153
|
+
].filter(Boolean).join("\n\n");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function handleConnectorRunMessage(
|
|
157
|
+
machine: MachineHeartbeat,
|
|
158
|
+
message: string,
|
|
159
|
+
send: (message: string) => void,
|
|
160
|
+
executeCli: LocalCliExecutor
|
|
161
|
+
) {
|
|
162
|
+
let parsed: any;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(message);
|
|
165
|
+
} catch {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (parsed?.type !== "agent.run" || typeof parsed.requestId !== "string") return;
|
|
169
|
+
const agentConfig = parsed.payload?.agentConfig ?? {};
|
|
170
|
+
try {
|
|
171
|
+
if (agentConfig.apiSource !== "cli") {
|
|
172
|
+
throw new Error("Connector can only execute CLI agents. Set the agent apiSource to cli and choose a cliProvider.");
|
|
173
|
+
}
|
|
174
|
+
const provider = String(agentConfig.cliProvider || "copilot");
|
|
175
|
+
const policy = resolveMachineRunPermissionPolicy(agentConfig);
|
|
176
|
+
const userInput = String(parsed.payload?.userInput ?? "");
|
|
177
|
+
assertMachineRunAllowed(userInput, policy);
|
|
178
|
+
const result = await executeCli(
|
|
179
|
+
provider,
|
|
180
|
+
buildConnectorCliPrompt(agentConfig, userInput),
|
|
181
|
+
{
|
|
182
|
+
model: agentConfig.model || undefined,
|
|
183
|
+
yolo: true,
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
send(JSON.stringify({
|
|
187
|
+
type: "agent.run.result",
|
|
188
|
+
requestId: parsed.requestId,
|
|
189
|
+
result: {
|
|
190
|
+
content: result.text,
|
|
191
|
+
model: agentConfig.model ?? provider,
|
|
192
|
+
trace: [{ role: "assistant", content: result.text }],
|
|
193
|
+
},
|
|
194
|
+
}));
|
|
195
|
+
} catch (error) {
|
|
196
|
+
send(JSON.stringify({
|
|
197
|
+
type: "agent.run.result",
|
|
198
|
+
requestId: parsed.requestId,
|
|
199
|
+
error: error instanceof Error ? error.message : String(error),
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function formatMachineStatus(machines: MachineSummary[]) {
|
|
205
|
+
if (machines.length === 0) {
|
|
206
|
+
return "No connected machines.\nRun `nolo connect` on this computer to register it once.\n";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return [
|
|
210
|
+
"Connected machines:",
|
|
211
|
+
...machines.map((machine) => {
|
|
212
|
+
const caps = machine.capabilities.length ? machine.capabilities.join(", ") : "no capabilities";
|
|
213
|
+
const connector = machine.connectorStatus ? ` ws:${machine.connectorStatus}` : "";
|
|
214
|
+
return `- ${machine.name} ${machine.status}${connector} ${machine.platform}/${machine.arch} ${caps}`;
|
|
215
|
+
}),
|
|
216
|
+
"",
|
|
217
|
+
].join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function runMachineConnectCommand(
|
|
221
|
+
args: string[],
|
|
222
|
+
deps: MachineCommandDeps = {}
|
|
223
|
+
) {
|
|
224
|
+
const env = deps.env ?? process.env;
|
|
225
|
+
const output = deps.output ?? process.stdout;
|
|
226
|
+
const authToken = resolveAuthToken(env);
|
|
227
|
+
if (!authToken) {
|
|
228
|
+
writeAuthMissing(output);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const serverUrl = resolveServerUrl(env);
|
|
233
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
234
|
+
const machine = (deps.machineInfo ?? detectLaunchableMachineInfo)();
|
|
235
|
+
const sendHeartbeat = async () => {
|
|
236
|
+
const res = await fetchImpl(`${serverUrl}/api/machines/heartbeat`, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers: {
|
|
239
|
+
Authorization: `Bearer ${authToken}`,
|
|
240
|
+
"Content-Type": "application/json",
|
|
241
|
+
},
|
|
242
|
+
body: JSON.stringify(machine),
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
if (!res.ok) {
|
|
246
|
+
const text = await res.text().catch(() => "");
|
|
247
|
+
throw new Error(`HTTP ${res.status}\n${text}`);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (hasFlag(args, "--daemon") || hasFlag(args, "--background")) {
|
|
252
|
+
const logPath = resolveDaemonLogPath(env);
|
|
253
|
+
const result = (deps.spawnDaemon ?? defaultSpawnDaemon)({
|
|
254
|
+
cmd: [process.execPath, import.meta.path, "connect", "--ws"],
|
|
255
|
+
cwd: process.cwd(),
|
|
256
|
+
env,
|
|
257
|
+
logPath,
|
|
258
|
+
});
|
|
259
|
+
output.write(`Connector daemon started${result.pid ? ` pid=${result.pid}` : ""}. Log: ${logPath}\n`);
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (hasFlag(args, "--ws")) {
|
|
264
|
+
const sentMessages: string[] = [];
|
|
265
|
+
const heartbeatAbort = new AbortController();
|
|
266
|
+
try {
|
|
267
|
+
await sendHeartbeat();
|
|
268
|
+
output.write(`Connector websocket connected: ${machine.name} (${machine.machineId})\n`);
|
|
269
|
+
const heartbeatLoopPromise = (deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop)({
|
|
270
|
+
intervalMs: resolveHeartbeatIntervalMs(env),
|
|
271
|
+
sendHeartbeat,
|
|
272
|
+
signal: heartbeatAbort.signal,
|
|
273
|
+
});
|
|
274
|
+
const wsTarget = await resolveConnectorWebSocketTarget({
|
|
275
|
+
serverUrl,
|
|
276
|
+
machineId: machine.machineId,
|
|
277
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
278
|
+
fetchImpl,
|
|
279
|
+
});
|
|
280
|
+
const websocketPromise = (deps.connectWebSocket ?? defaultConnectWebSocket)(
|
|
281
|
+
wsTarget,
|
|
282
|
+
{
|
|
283
|
+
headers: { Authorization: `Bearer ${authToken}` },
|
|
284
|
+
sentMessages,
|
|
285
|
+
onMessage: (message) => handleConnectorRunMessage(
|
|
286
|
+
machine,
|
|
287
|
+
message,
|
|
288
|
+
(response) => sentMessages.push(response),
|
|
289
|
+
deps.executeCli ?? defaultExecuteCli
|
|
290
|
+
),
|
|
291
|
+
}
|
|
292
|
+
);
|
|
293
|
+
await Promise.race([websocketPromise, heartbeatLoopPromise]);
|
|
294
|
+
heartbeatAbort.abort();
|
|
295
|
+
await Promise.allSettled([websocketPromise, heartbeatLoopPromise]);
|
|
296
|
+
return 0;
|
|
297
|
+
} catch (error) {
|
|
298
|
+
heartbeatAbort.abort();
|
|
299
|
+
output.write(
|
|
300
|
+
`[nolo] Connector websocket failed: ${
|
|
301
|
+
error instanceof Error ? error.message : String(error)
|
|
302
|
+
}\n`
|
|
303
|
+
);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (hasFlag(args, "--watch")) {
|
|
309
|
+
output.write(`Connecting machine heartbeat loop: ${machine.name} (${machine.platform}/${machine.arch})\n`);
|
|
310
|
+
try {
|
|
311
|
+
await (deps.runHeartbeatLoop ?? defaultRunHeartbeatLoop)({
|
|
312
|
+
intervalMs: resolveHeartbeatIntervalMs(env),
|
|
313
|
+
sendHeartbeat,
|
|
314
|
+
});
|
|
315
|
+
return 0;
|
|
316
|
+
} catch (error) {
|
|
317
|
+
output.write(
|
|
318
|
+
`[nolo] Machine heartbeat loop failed: ${
|
|
319
|
+
error instanceof Error ? error.message : String(error)
|
|
320
|
+
}\n`
|
|
321
|
+
);
|
|
322
|
+
return 1;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
await sendHeartbeat();
|
|
328
|
+
} catch (error) {
|
|
329
|
+
output.write(
|
|
330
|
+
`[nolo] Machine connect failed: ${
|
|
331
|
+
error instanceof Error ? error.message : String(error)
|
|
332
|
+
}\n`
|
|
333
|
+
);
|
|
334
|
+
return 1;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
output.write(`Connected machine: ${machine.name} (${machine.platform}/${machine.arch})\n`);
|
|
338
|
+
return 0;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export async function runMachineStatusCommand(
|
|
342
|
+
_args: string[],
|
|
343
|
+
deps: MachineCommandDeps = {}
|
|
344
|
+
) {
|
|
345
|
+
const env = deps.env ?? process.env;
|
|
346
|
+
const output = deps.output ?? process.stdout;
|
|
347
|
+
const authToken = resolveAuthToken(env);
|
|
348
|
+
if (!authToken) {
|
|
349
|
+
writeAuthMissing(output);
|
|
350
|
+
return 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const serverUrl = resolveServerUrl(env);
|
|
354
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
355
|
+
let res: Response;
|
|
356
|
+
try {
|
|
357
|
+
res = await fetchImpl(`${serverUrl}/api/machines`, {
|
|
358
|
+
method: "GET",
|
|
359
|
+
headers: {
|
|
360
|
+
Authorization: `Bearer ${authToken}`,
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
} catch (error) {
|
|
364
|
+
output.write(
|
|
365
|
+
`[nolo] Machine status failed: ${
|
|
366
|
+
error instanceof Error ? error.message : String(error)
|
|
367
|
+
}\n`
|
|
368
|
+
);
|
|
369
|
+
return 1;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (!res.ok) {
|
|
373
|
+
const text = await res.text().catch(() => "");
|
|
374
|
+
output.write(`[nolo] Machine status failed: HTTP ${res.status}\n${text}\n`);
|
|
375
|
+
return 1;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const data = await res.json().catch(() => ({ machines: [] }));
|
|
379
|
+
const machines = Array.isArray(data?.machines) ? data.machines : [];
|
|
380
|
+
output.write(formatMachineStatus(machines));
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nolo-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first terminal workspace for Nolo",
|
|
6
6
|
"bin": {
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
"module": "index.ts",
|
|
10
10
|
"files": [
|
|
11
11
|
"index.ts",
|
|
12
|
+
"agentRuntimeCommands.ts",
|
|
12
13
|
"authCommands.ts",
|
|
13
14
|
"commandRegistry.ts",
|
|
15
|
+
"connectorWebSocketTarget.ts",
|
|
16
|
+
"defaultServer.ts",
|
|
17
|
+
"machineCommands.ts",
|
|
14
18
|
"updateCommands.ts",
|
|
15
19
|
"client/agentRun.ts",
|
|
16
20
|
"client/profileConfig.ts",
|
|
@@ -24,6 +28,10 @@
|
|
|
24
28
|
"devDependencies": {
|
|
25
29
|
"bun-types": "latest"
|
|
26
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"ai": "workspace:*",
|
|
33
|
+
"connector-experimental": "workspace:*"
|
|
34
|
+
},
|
|
27
35
|
"peerDependencies": {
|
|
28
36
|
"typescript": "^5.0.0"
|
|
29
37
|
}
|