[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled

This commit is contained in:
2026-09-13 18:24:01 +08:00
commit 3c72efcb7f
3255 changed files with 907009 additions and 0 deletions

View File

@@ -0,0 +1,548 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const {
buildCursorAgentOptions,
buildCursorSendMessage,
formatCursorErrorForUser,
isCursorAgentNotFoundError,
mapCursorModels,
runCursorTurn,
toCursorMcpServers,
translateCursorEvent,
withTemporaryProcessEnv,
} = require("./cursorDriver.cjs");
function makeEmitter() {
const calls = [];
return {
calls,
text: (value) => calls.push(["text", value]),
reasoning: (value) => calls.push(["reasoning", value]),
reasoningEnd: () => calls.push(["reasoningEnd"]),
toolCall: (name, args, id) => calls.push(["toolCall", name, args, id]),
toolResult: (id, result, name) => calls.push(["toolResult", id, result, name]),
sessionId: (id) => calls.push(["sessionId", id]),
emitDone: () => calls.push(["done"]),
emitError: (message) => calls.push(["error", message]),
};
}
test("buildCursorAgentOptions uses api key, model, cwd, and injected MCP servers", () => {
const options = buildCursorAgentOptions({
apiKey: "cur-key",
model: "composer-2",
cwd: "/repo",
injectedMcpServers: [
{
name: "netcatty",
command: "node",
args: ["server.cjs"],
env: [{ name: "TOKEN", value: "abc" }],
},
],
});
assert.deepEqual(options, {
apiKey: "cur-key",
model: { id: "composer-2" },
local: { cwd: "/repo", autoReview: false },
mcpServers: {
netcatty: {
type: "stdio",
command: "node",
args: ["server.cjs"],
env: { TOKEN: "abc" },
},
},
});
});
test("buildCursorAgentOptions falls back to CURSOR_API_KEY and composer-2.5", () => {
const options = buildCursorAgentOptions({
env: { CURSOR_API_KEY: "env-key" },
cwd: "/repo",
});
assert.equal(options.apiKey, "env-key");
assert.deepEqual(options.model, { id: "composer-2.5" });
});
test("toCursorMcpServers drops invalid server configs", () => {
assert.deepEqual(
toCursorMcpServers([
null,
{ name: "", command: "node" },
{ name: "ok", command: "node", args: [] },
]),
{ ok: { type: "stdio", command: "node", args: [], env: {} } },
);
});
test("withTemporaryProcessEnv restores env after async work", async () => {
const original = process.env.NETCATTY_CURSOR_TEST_ENV;
delete process.env.NETCATTY_CURSOR_TEST_ENV;
const value = await withTemporaryProcessEnv(
{ NETCATTY_CURSOR_TEST_ENV: "present" },
async () => process.env.NETCATTY_CURSOR_TEST_ENV,
);
assert.equal(value, "present");
assert.equal(process.env.NETCATTY_CURSOR_TEST_ENV, undefined);
if (original !== undefined) process.env.NETCATTY_CURSOR_TEST_ENV = original;
});
test("runCursorTurn exposes runtime env while creating and sending", async () => {
const emitter = makeEmitter();
const observed = [];
const sdkModule = {
Agent: {
async create() {
observed.push(["create", process.env.NETCATTY_TOOL_CLI_DISCOVERY_FILE]);
return {
agentId: "agent-env",
async send() {
observed.push(["send", process.env.NETCATTY_TOOL_CLI_DISCOVERY_FILE]);
return {
async *stream() {
yield { type: "assistant", message: { content: [{ type: "text", text: "ok" }] } };
},
};
},
close() {},
};
},
},
};
await runCursorTurn({
prompt: "hi",
agentOptions: { apiKey: "key", model: { id: "composer-2.5" }, local: { cwd: "/repo" } },
runtimeEnv: { NETCATTY_TOOL_CLI_DISCOVERY_FILE: "/tmp/discovery.json" },
emitter,
sdkModule,
});
assert.deepEqual(observed, [
["create", "/tmp/discovery.json"],
["send", "/tmp/discovery.json"],
]);
});
test("translateCursorEvent maps assistant, thinking, and tool events", () => {
const emitter = makeEmitter();
const state = {};
translateCursorEvent({ type: "thinking", text: "checking" }, emitter, state);
translateCursorEvent({
type: "assistant",
message: {
content: [
{ type: "text", text: "hello" },
{ type: "tool_use", id: "tool-1", name: "read_file", input: { path: "README.md" } },
],
},
}, emitter, state);
translateCursorEvent({
type: "tool_call",
call_id: "tool-1",
name: "read_file",
status: "completed",
result: { content: [{ type: "text", text: "contents" }] },
}, emitter, state);
assert.deepEqual(emitter.calls, [
["reasoning", "checking"],
["reasoningEnd"],
["text", "hello"],
["toolCall", "read_file", { path: "README.md" }, "tool-1"],
["toolResult", "tool-1", "contents", "read_file"],
]);
});
test("translateCursorEvent uses nested Cursor MCP toolName for display", () => {
const emitter = makeEmitter();
const state = {};
const args = {
providerIdentifier: "netcatty-remote-hosts",
toolName: "terminal_execute",
args: { command: "uname -a" },
};
translateCursorEvent({
type: "tool_call",
call_id: "mcp-1",
name: "mcp",
status: "completed",
args,
result: { content: [{ type: "text", text: "Linux" }] },
}, emitter, state);
assert.deepEqual(emitter.calls, [
["toolCall", "terminal_execute", args, "mcp-1"],
["toolResult", "mcp-1", "Linux", "terminal_execute"],
]);
});
test("translateCursorEvent marks error status as failed", () => {
const emitter = makeEmitter();
const state = {};
const failed = translateCursorEvent({ type: "status", status: "ERROR", message: "bad key" }, emitter, state);
assert.equal(failed, true);
assert.equal(state.failed, true);
assert.deepEqual(emitter.calls, [["error", "bad key"]]);
});
test("translateCursorEvent rewrites Cursor authentication errors", () => {
const emitter = makeEmitter();
const state = {};
const failed = translateCursorEvent({ type: "status", status: "ERROR", message: "bad API key" }, emitter, state);
assert.equal(failed, true);
assert.equal(state.failed, true);
assert.deepEqual(emitter.calls, [[
"error",
"Cursor authentication failed. Update the Cursor API Key in Settings -> AI.",
]]);
});
test("formatCursorErrorForUser points users to the settings API key", () => {
assert.equal(
formatCursorErrorForUser("unauthorized"),
"Cursor authentication failed. Update the Cursor API Key in Settings -> AI.",
);
});
test("isCursorAgentNotFoundError detects stale resume ids", () => {
assert.equal(isCursorAgentNotFoundError(new Error("Agent 61668441-bfcb-4795-a575-c46d70ad01fe not found")), true);
assert.equal(isCursorAgentNotFoundError(new Error("unauthorized")), false);
});
test("runCursorTurn falls back to create when resume agent is missing", async () => {
const emitter = makeEmitter();
const observed = [];
const sdkModule = {
Agent: {
async resume(id) {
observed.push(["resume", id]);
throw new Error(`Agent ${id} not found`);
},
async create() {
observed.push(["create"]);
return {
agentId: "agent-fresh",
async send() {
return {
async *stream() {
yield { type: "assistant", message: { content: [{ type: "text", text: "ok" }] } };
},
};
},
close() {},
};
},
},
};
const result = await runCursorTurn({
prompt: "hi",
resumeSessionId: "61668441-bfcb-4795-a575-c46d70ad01fe",
agentOptions: { apiKey: "key", model: { id: "composer-2.5" }, local: { cwd: "/repo" } },
emitter,
sdkModule,
});
assert.deepEqual(observed, [
["resume", "61668441-bfcb-4795-a575-c46d70ad01fe"],
["create"],
]);
assert.equal(result.sessionId, "agent-fresh");
assert.deepEqual(emitter.calls, [
["sessionId", "agent-fresh"],
["text", "ok"],
["done"],
]);
});
test("runCursorTurn creates or resumes an agent, streams events, and emits done", async () => {
const emitter = makeEmitter();
const captured = {};
const sdkModule = {
Agent: {
async create(options) {
captured.createOptions = options;
return {
agentId: "agent-new",
async send(message) {
captured.message = message;
return {
id: "run-1",
agentId: "agent-new",
async *stream() {
yield { type: "assistant", message: { content: [{ type: "text", text: "done" }] } };
},
};
},
async close() {
captured.closed = true;
},
};
},
},
};
const result = await runCursorTurn({
prompt: "hi",
attachments: [{ mediaType: "image/png", base64Data: "abc", filename: "a.png" }],
agentOptions: { apiKey: "key", model: { id: "composer-2" }, local: { cwd: "/repo" } },
emitter,
sdkModule,
});
assert.equal(result.sessionId, "agent-new");
assert.deepEqual(captured.message, {
text: "hi",
images: [{ data: "abc", mimeType: "image/png" }],
});
assert.deepEqual(emitter.calls, [
["sessionId", "agent-new"],
["text", "done"],
["done"],
]);
assert.equal(captured.closed, true);
});
test("runCursorTurn does not emit done after a Cursor error status", async () => {
const emitter = makeEmitter();
const sdkModule = {
Agent: {
async create() {
return {
agentId: "agent-error",
async send() {
return {
async *stream() {
yield { type: "status", status: "ERROR", message: "bad key" };
yield { type: "assistant", message: { content: [{ type: "text", text: "late" }] } };
},
};
},
close() {},
};
},
},
};
const result = await runCursorTurn({
prompt: "hi",
agentOptions: { apiKey: "key", model: { id: "composer-2.5" }, local: { cwd: "/repo" } },
emitter,
sdkModule,
});
assert.equal(result.sessionId, "agent-error");
assert.deepEqual(emitter.calls, [
["sessionId", "agent-error"],
["error", "bad key"],
]);
});
test("runCursorTurn returns when aborted while creating an agent", async () => {
const emitter = makeEmitter();
let resolveCreate;
const createPromise = new Promise((resolve) => {
resolveCreate = resolve;
});
const sdkModule = {
Agent: {
create() {
return createPromise;
},
},
};
const controller = new AbortController();
const turnPromise = runCursorTurn({
prompt: "hi",
agentOptions: { apiKey: "key", model: { id: "composer-2.5" }, local: { cwd: "/repo" } },
emitter,
signal: controller.signal,
sdkModule,
});
controller.abort();
const result = await turnPromise;
assert.deepEqual(result, { sessionId: null });
assert.deepEqual(emitter.calls, []);
let closed = false;
resolveCreate({ agentId: "late", close: () => { closed = true; } });
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(closed, true);
});
test("runCursorTurn restores runtime env when aborted while creating an agent", async () => {
const emitter = makeEmitter();
const original = process.env.NETCATTY_CURSOR_ABORT_ENV;
delete process.env.NETCATTY_CURSOR_ABORT_ENV;
const sdkModule = {
Agent: {
create() {
return new Promise(() => {});
},
},
};
const controller = new AbortController();
const turnPromise = runCursorTurn({
prompt: "hi",
agentOptions: { apiKey: "key", model: { id: "composer-2.5" }, local: { cwd: "/repo" } },
runtimeEnv: { NETCATTY_CURSOR_ABORT_ENV: "present" },
emitter,
signal: controller.signal,
sdkModule,
});
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(process.env.NETCATTY_CURSOR_ABORT_ENV, "present");
controller.abort();
await turnPromise;
assert.equal(process.env.NETCATTY_CURSOR_ABORT_ENV, undefined);
if (original !== undefined) process.env.NETCATTY_CURSOR_ABORT_ENV = original;
});
test("runCursorTurn cancels a late Cursor run when aborted while sending", async () => {
const emitter = makeEmitter();
let resolveSend;
let cancelled = false;
const sendPromise = new Promise((resolve) => {
resolveSend = resolve;
});
const sdkModule = {
Agent: {
async create() {
return {
agentId: "agent-send-abort",
send() {
return sendPromise;
},
close() {},
};
},
},
};
const controller = new AbortController();
const turnPromise = runCursorTurn({
prompt: "hi",
agentOptions: { apiKey: "key", model: { id: "composer-2.5" }, local: { cwd: "/repo" } },
emitter,
signal: controller.signal,
sdkModule,
});
await new Promise((resolve) => setTimeout(resolve, 0));
controller.abort();
const result = await turnPromise;
assert.deepEqual(result, { sessionId: "agent-send-abort" });
assert.deepEqual(emitter.calls, [["sessionId", "agent-send-abort"]]);
resolveSend({ cancel: async () => { cancelled = true; }, stream: async function* stream() {} });
await new Promise((resolve) => setTimeout(resolve, 0));
assert.equal(cancelled, true);
});
test("mapCursorModels prefers advertised effort parameter values over fallbacks", () => {
assert.deepEqual(
mapCursorModels([
{
id: "custom-reasoner",
displayName: "Custom Reasoner",
parameters: [
{ id: "effort", values: [{ value: "low" }, { value: "xhigh" }] },
],
},
{
id: "gpt-5",
displayName: "GPT-5",
parameters: [
{ id: "effort", values: [{ value: "low" }, { value: "high" }] },
],
},
]),
[
{
id: "custom-reasoner",
name: "Custom Reasoner",
thinkingLevels: ["low", "xhigh"],
defaultThinkingLevel: "low",
},
{
id: "gpt-5",
name: "GPT-5",
thinkingLevels: ["low", "high"],
defaultThinkingLevel: "low",
},
],
);
});
test("mapCursorModels maps display names and effort variants into thinkingLevels", () => {
assert.deepEqual(
mapCursorModels([
{ id: "composer-2.5", displayName: "Composer 2.5", description: "Default" },
{ id: "gpt-5", displayName: "GPT-5", variants: [{ displayName: "Fast", params: [{ id: "effort", value: "low" }] }] },
]),
[
{ id: "composer-2.5", name: "Composer 2.5", description: "Default" },
{
id: "gpt-5",
name: "GPT-5",
thinkingLevels: ["low", "medium", "high"],
defaultThinkingLevel: "medium",
},
],
);
});
test("mapCursorModels keeps extra-param variants as separate models", () => {
const mapped = mapCursorModels([
{
id: "gpt-5",
displayName: "GPT-5",
variants: [
{ displayName: "Fast", params: [{ id: "effort", value: "low" }] },
{
displayName: "Fast custom",
params: [{ id: "effort", value: "low" }, { id: "mode", value: "fast" }],
},
],
},
]);
assert.deepEqual(mapped, [
{
id: "gpt-5",
name: "GPT-5",
thinkingLevels: ["low", "medium", "high"],
defaultThinkingLevel: "medium",
},
{
id: "gpt-5?effort=low&mode=fast",
name: "GPT-5 - Fast custom",
},
]);
});
test("parseCursorModelSelection accepts query and slash effort encodings", () => {
const { parseCursorModelSelection, encodeCursorCliModel } = require("./cursorDriver.cjs");
assert.deepEqual(parseCursorModelSelection("gpt-5/high"), {
id: "gpt-5",
params: [{ id: "effort", value: "high" }],
});
assert.deepEqual(parseCursorModelSelection("gpt-5?effort=low"), {
id: "gpt-5",
params: [{ id: "effort", value: "low" }],
});
assert.equal(encodeCursorCliModel("gpt-5/high"), "gpt-5?effort=high");
});