cueloop 0.1.0-alpha.102 → 0.1.0-alpha.103
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/package.json +6 -6
- package/src/actions-command.ts +2 -0
- package/src/agent-harness.ts +10 -8
- package/src/args.ts +1 -0
- package/src/codex-delivery-worker-command.test.ts +3 -0
- package/src/codex-delivery-worker-command.ts +5 -0
- package/src/codex-mcp-command.ts +1 -0
- package/src/daemon-command.ts +10 -3
- package/src/daemon-control.ts +5 -0
- package/src/dev-command.ts +8 -3
- package/src/harness-selection.test.ts +22 -0
- package/src/harness-selection.ts +45 -0
- package/src/install-command.test.ts +2 -0
- package/src/install-command.ts +5 -0
- package/src/main.ts +33 -2
- package/src/open-target.ts +1 -0
- package/src/pr.ts +26 -0
- package/src/share-command.ts +6 -0
- package/src/thread-commands.ts +11 -0
- package/src/update-command.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cueloop",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.103",
|
|
4
4
|
"description": "Terminal review surface for coding agents: annotate plans and diffs, return a message, send structured feedback back",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
"build:binary": "bun build --compile --bytecode --format=esm --minify --external cpu-features ./src/main.ts --outfile dist/cueloop"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@cueloop/adapters": "0.1.0-alpha.
|
|
34
|
-
"@cueloop/client": "0.1.0-alpha.
|
|
35
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
36
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
37
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
33
|
+
"@cueloop/adapters": "0.1.0-alpha.103",
|
|
34
|
+
"@cueloop/client": "0.1.0-alpha.103",
|
|
35
|
+
"@cueloop/daemon": "0.1.0-alpha.103",
|
|
36
|
+
"@cueloop/extension-api": "0.1.0-alpha.103",
|
|
37
|
+
"@cueloop/schema": "0.1.0-alpha.103",
|
|
38
38
|
"@modelcontextprotocol/server": "2.1.0",
|
|
39
39
|
"@valibot/to-json-schema": "1.8.0",
|
|
40
40
|
"valibot": "^1.5.0"
|
package/src/actions-command.ts
CHANGED
|
@@ -19,6 +19,7 @@ export async function actionsCommand(argv: string[]): Promise<number> {
|
|
|
19
19
|
|
|
20
20
|
return 2;
|
|
21
21
|
}
|
|
22
|
+
|
|
22
23
|
const role = stringFlag(flags, "role") === "agent" ? "agent" : undefined;
|
|
23
24
|
const repoRoot = await repoRootForSession(stringFlag(flags, "session"), role);
|
|
24
25
|
const actions = loadConfig({ repoRoot }).actions;
|
|
@@ -39,6 +40,7 @@ async function repoRootForSession(
|
|
|
39
40
|
role: "agent" | undefined,
|
|
40
41
|
): Promise<string> {
|
|
41
42
|
if (sessionId === undefined) return process.cwd();
|
|
43
|
+
|
|
42
44
|
const client = await DaemonClient.connect({ autostart: true, role });
|
|
43
45
|
|
|
44
46
|
try {
|
package/src/agent-harness.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { createPiHarness } from "@cueloop/adapters/pi/durable-harness";
|
|
2
2
|
import { cueloopHome } from "@cueloop/daemon";
|
|
3
|
+
import { createThreadHarness } from "@cueloop/adapters/thread-harness";
|
|
3
4
|
import { createFxHarness } from "@cueloop/adapters/fx/harness";
|
|
4
5
|
import { loadConfig } from "@cueloop/client/config";
|
|
5
6
|
import type { AgentHarnessAdapter } from "@cueloop/schema";
|
|
6
7
|
|
|
7
|
-
/**
|
|
8
|
-
export function
|
|
9
|
-
const id = process.env.CUELOOP_AGENT_HARNESS ??
|
|
8
|
+
/** Explicit startup selection overrides the owner and workspace preferences. */
|
|
9
|
+
export function configuredAgentHarnessId(repoRoot?: string): "pi" | "fx" {
|
|
10
|
+
const id = process.env.CUELOOP_AGENT_HARNESS ?? loadConfig({ repoRoot }).thread.harness;
|
|
10
11
|
|
|
11
|
-
if (id
|
|
12
|
+
if (id !== "pi" && id !== "fx") throw new Error(`Thread agent harness is not installed: ${id}`);
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return fxHarness();
|
|
14
|
+
return id;
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
/** Resolve the experimental Thread agent flag from user and workspace TOML. */
|
|
@@ -28,5 +27,8 @@ export function installedAgentHarnesses() {
|
|
|
28
27
|
}
|
|
29
28
|
|
|
30
29
|
function fxHarness(): AgentHarnessAdapter {
|
|
31
|
-
return
|
|
30
|
+
return createThreadHarness({
|
|
31
|
+
home: cueloopHome(),
|
|
32
|
+
backend: createFxHarness({ command: [process.env.CUELOOP_FX_EXECUTABLE ?? "fx", "acp"] }),
|
|
33
|
+
});
|
|
32
34
|
}
|
package/src/args.ts
CHANGED
|
@@ -38,6 +38,7 @@ test("a Codex reply is delivered without an MCP transport", async () => {
|
|
|
38
38
|
);
|
|
39
39
|
|
|
40
40
|
if (opened.operation !== "open") throw new Error("expected open Thread");
|
|
41
|
+
|
|
41
42
|
const previousCodexBin = process.env.CUELOOP_CODEX_BIN;
|
|
42
43
|
|
|
43
44
|
// A reused PID belonging to this test process must not impersonate the worker.
|
|
@@ -49,8 +50,10 @@ test("a Codex reply is delivered without an MCP transport", async () => {
|
|
|
49
50
|
import.meta.dir + "/main.ts",
|
|
50
51
|
"codex-delivery-worker",
|
|
51
52
|
]);
|
|
53
|
+
|
|
52
54
|
if (previousCodexBin === undefined) delete process.env.CUELOOP_CODEX_BIN;
|
|
53
55
|
else process.env.CUELOOP_CODEX_BIN = previousCodexBin;
|
|
56
|
+
|
|
54
57
|
expect(worker).toBeDefined();
|
|
55
58
|
const deadline = Date.now() + 5000;
|
|
56
59
|
|
|
@@ -17,6 +17,7 @@ function claimWorker(path: string): boolean {
|
|
|
17
17
|
return true;
|
|
18
18
|
} catch (error) {
|
|
19
19
|
if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) throw error;
|
|
20
|
+
|
|
20
21
|
const before = statSync(path);
|
|
21
22
|
const owner = Number(readFileSync(path, "utf8"));
|
|
22
23
|
|
|
@@ -39,7 +40,9 @@ function claimWorker(path: string): boolean {
|
|
|
39
40
|
} else if (Date.now() - before.mtimeMs < 5_000) {
|
|
40
41
|
return false;
|
|
41
42
|
}
|
|
43
|
+
|
|
42
44
|
if (statSync(path).ino !== before.ino) return false;
|
|
45
|
+
|
|
43
46
|
unlinkSync(path);
|
|
44
47
|
}
|
|
45
48
|
}
|
|
@@ -53,6 +56,7 @@ export async function codexDeliveryWorkerCommand(): Promise<number> {
|
|
|
53
56
|
const lockPath = join(home, "codex-delivery-worker.pid");
|
|
54
57
|
|
|
55
58
|
if (!claimWorker(lockPath)) return 0;
|
|
59
|
+
|
|
56
60
|
const sessions = createCodexSessionRegistry(home);
|
|
57
61
|
const delivery = createCodexDeliveryService({
|
|
58
62
|
home,
|
|
@@ -70,6 +74,7 @@ export async function codexDeliveryWorkerCommand(): Promise<number> {
|
|
|
70
74
|
}
|
|
71
75
|
} finally {
|
|
72
76
|
delivery.stop();
|
|
77
|
+
|
|
73
78
|
if (readFileSync(lockPath, "utf8") === String(process.pid)) unlinkSync(lockPath);
|
|
74
79
|
}
|
|
75
80
|
|
package/src/codex-mcp-command.ts
CHANGED
|
@@ -41,6 +41,7 @@ export function createCodexMcpServer(home = cueloopHome()): McpServer {
|
|
|
41
41
|
"cueloop Codex session is not authorized; trust the plugin hooks and restart Codex",
|
|
42
42
|
);
|
|
43
43
|
}
|
|
44
|
+
|
|
44
45
|
const { hookToken: _hookToken, ...threadInput } = input;
|
|
45
46
|
const result = await runHarnessBridge(
|
|
46
47
|
{ operation: "open", harness: "codex", ...threadInput },
|
package/src/daemon-command.ts
CHANGED
|
@@ -8,20 +8,26 @@ export async function daemonCommand(argv: string[]): Promise<number> {
|
|
|
8
8
|
let idleExitMs: number | undefined = 0;
|
|
9
9
|
|
|
10
10
|
if (argv.includes("--autostart")) idleExitMs = envIdle ? Number(envIdle) : undefined;
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
const { configuredAgentHarnessId, installedAgentHarnesses, threadAgentEnabled } =
|
|
12
13
|
await import("./agent-harness");
|
|
14
|
+
const adapters = installedAgentHarnesses();
|
|
13
15
|
let relay: OwnerAgentRelay | undefined;
|
|
14
16
|
const server = new DaemonServer({
|
|
15
17
|
onEvent: (event) => {
|
|
16
18
|
relay?.notify(event);
|
|
19
|
+
|
|
17
20
|
if (event.event !== "agent.updated") relay?.update(server.core.sessionList());
|
|
18
21
|
},
|
|
19
22
|
idleExitMs,
|
|
23
|
+
agentHarnessDefault: () => configuredAgentHarnessId(),
|
|
20
24
|
threadAgent: {
|
|
21
25
|
enabled: true,
|
|
22
26
|
enabledForThread: (thread) => threadAgentEnabled(thread.workspace.repoRoot),
|
|
23
|
-
adapter:
|
|
24
|
-
adapters
|
|
27
|
+
adapter: adapters[configuredAgentHarnessId()],
|
|
28
|
+
adapters,
|
|
29
|
+
defaultHarnessForThread: (thread) =>
|
|
30
|
+
adapters[configuredAgentHarnessId(thread.workspace.repoRoot)],
|
|
25
31
|
},
|
|
26
32
|
});
|
|
27
33
|
const path = server.start();
|
|
@@ -31,6 +37,7 @@ export async function daemonCommand(argv: string[]): Promise<number> {
|
|
|
31
37
|
|
|
32
38
|
return 1;
|
|
33
39
|
}
|
|
40
|
+
|
|
34
41
|
server.resumeAgents();
|
|
35
42
|
relay = new OwnerAgentRelay({
|
|
36
43
|
home: server.home,
|
package/src/daemon-control.ts
CHANGED
|
@@ -21,6 +21,7 @@ function signalDaemonByProcessId(home: string): boolean {
|
|
|
21
21
|
const path = pidPath(home);
|
|
22
22
|
|
|
23
23
|
if (!existsSync(path)) return false;
|
|
24
|
+
|
|
24
25
|
let raw: string;
|
|
25
26
|
|
|
26
27
|
try {
|
|
@@ -31,6 +32,7 @@ function signalDaemonByProcessId(home: string): boolean {
|
|
|
31
32
|
const parsed = v.safeParse(ProcessIdSchema, raw);
|
|
32
33
|
|
|
33
34
|
if (!parsed.success) return false;
|
|
35
|
+
|
|
34
36
|
try {
|
|
35
37
|
process.kill(parsed.output);
|
|
36
38
|
|
|
@@ -47,6 +49,7 @@ async function waitForSocketGone(home: string): Promise<boolean> {
|
|
|
47
49
|
|
|
48
50
|
while (Date.now() < deadline) {
|
|
49
51
|
if (!existsSync(path)) return true;
|
|
52
|
+
|
|
50
53
|
await Bun.sleep(50);
|
|
51
54
|
}
|
|
52
55
|
|
|
@@ -62,6 +65,7 @@ async function waitForSocketGone(home: string): Promise<boolean> {
|
|
|
62
65
|
*/
|
|
63
66
|
export async function stopDaemon(home = cueloopHome()): Promise<boolean> {
|
|
64
67
|
if (!existsSync(socketPath(home))) return false;
|
|
68
|
+
|
|
65
69
|
try {
|
|
66
70
|
const client = await DaemonClient.connect({ home, autostart: false });
|
|
67
71
|
|
|
@@ -83,6 +87,7 @@ export async function stopCommand(): Promise<number> {
|
|
|
83
87
|
|
|
84
88
|
return 0;
|
|
85
89
|
}
|
|
90
|
+
|
|
86
91
|
const stopped = await stopDaemon(home);
|
|
87
92
|
|
|
88
93
|
console.log(stopped ? "cueloop daemon stopped" : "cueloop daemon did not stop in time");
|
package/src/dev-command.ts
CHANGED
|
@@ -97,17 +97,22 @@ async function refreshDevSeed(client: DaemonClient, home: string): Promise<void>
|
|
|
97
97
|
writeFileSync(versionFile, String(SEED_VERSION));
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const home = (process.env.CUELOOP_HOME ??= threadAgentEnabled(process.cwd())
|
|
100
|
+
export function configureDevHome(): string {
|
|
101
|
+
return (process.env.CUELOOP_HOME ??= threadAgentEnabled(process.cwd())
|
|
103
102
|
? join(process.cwd(), ".cueloop-dev")
|
|
104
103
|
: join(homedir(), ".cueloop-dev"));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** `cueloop dev`: seed an isolated dev home (refreshing a stale seed), then open the TUI on it. */
|
|
107
|
+
export async function devCommand(): Promise<number> {
|
|
108
|
+
const home = configureDevHome();
|
|
105
109
|
|
|
106
110
|
let sessionId: string | undefined;
|
|
107
111
|
const client = await DaemonClient.connect({ autostart: true });
|
|
108
112
|
|
|
109
113
|
try {
|
|
110
114
|
await refreshDevSeed(client, home);
|
|
115
|
+
|
|
111
116
|
if (threadAgentEnabled(process.cwd()))
|
|
112
117
|
sessionId = (await client.sessionList()).find(
|
|
113
118
|
(thread) => thread.artifact.meta.title === "Read the repository",
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { parseHarnessSelection } from "./harness-selection";
|
|
3
|
+
|
|
4
|
+
test("harness selection supports launcher and restart without stealing session flags", () => {
|
|
5
|
+
expect(parseHarnessSelection(["--harness", "fx"])).toEqual({ args: [], harness: "fx" });
|
|
6
|
+
expect(parseHarnessSelection(["restart", "--harness=pi"])).toEqual({
|
|
7
|
+
args: ["restart"],
|
|
8
|
+
harness: "pi",
|
|
9
|
+
});
|
|
10
|
+
expect(parseHarnessSelection(["--harness", "fx", "plan", "thread-id"])).toEqual({
|
|
11
|
+
args: ["plan", "thread-id"],
|
|
12
|
+
harness: "fx",
|
|
13
|
+
});
|
|
14
|
+
expect(parseHarnessSelection(["session", "bind-harness", "--harness", "codex"])).toEqual({
|
|
15
|
+
args: ["session", "bind-harness", "--harness", "codex"],
|
|
16
|
+
});
|
|
17
|
+
expect(() => parseHarnessSelection(["restart", "--harness"])).toThrow(
|
|
18
|
+
"--harness pi or --harness fx",
|
|
19
|
+
);
|
|
20
|
+
expect(() => parseHarnessSelection(["--harness=bad"])).toThrow("--harness pi or --harness fx");
|
|
21
|
+
expect(() => parseHarnessSelection(["--harness=pi", "--harness=fx"])).toThrow("only one");
|
|
22
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { DaemonClient } from "@cueloop/daemon/client";
|
|
2
|
+
|
|
3
|
+
/** Global harness flags never consume a session primitive's unrelated --harness argument. */
|
|
4
|
+
interface HarnessSelection {
|
|
5
|
+
args: string[];
|
|
6
|
+
harness?: "pi" | "fx";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function parseHarnessSelection(args: readonly string[]): HarnessSelection {
|
|
10
|
+
const remaining = [...args];
|
|
11
|
+
const global = args[0]?.startsWith("--harness") || args[0] === "restart" || args[0] === "daemon";
|
|
12
|
+
|
|
13
|
+
if (!global) return { args: remaining };
|
|
14
|
+
|
|
15
|
+
const index = remaining.findIndex((arg) => arg === "--harness" || arg.startsWith("--harness="));
|
|
16
|
+
|
|
17
|
+
if (index < 0) return { args: remaining };
|
|
18
|
+
|
|
19
|
+
const flag = remaining[index]!;
|
|
20
|
+
const value = flag === "--harness" ? remaining[index + 1] : flag.slice("--harness=".length);
|
|
21
|
+
|
|
22
|
+
if (value !== "pi" && value !== "fx")
|
|
23
|
+
throw new Error("Thread harness selection requires --harness pi or --harness fx");
|
|
24
|
+
|
|
25
|
+
remaining.splice(index, flag === "--harness" ? 2 : 1);
|
|
26
|
+
|
|
27
|
+
if (remaining.some((arg) => arg === "--harness" || arg.startsWith("--harness=")))
|
|
28
|
+
throw new Error("Thread harness selection accepts only one --harness flag");
|
|
29
|
+
|
|
30
|
+
return { args: remaining, harness: value };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A running daemon must confirm the requested default before a launcher opens its UI. */
|
|
34
|
+
export async function assertDaemonHarness(harness: "pi" | "fx"): Promise<void> {
|
|
35
|
+
const client = await DaemonClient.connect({ autostart: true });
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const current = await client.ping();
|
|
39
|
+
|
|
40
|
+
if (current.agentHarness !== harness)
|
|
41
|
+
throw new Error(`Thread harness default mismatch. Run cueloop restart --harness ${harness}.`);
|
|
42
|
+
} finally {
|
|
43
|
+
client.close();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -74,6 +74,7 @@ describe("cueloop install", () => {
|
|
|
74
74
|
installRoot: root,
|
|
75
75
|
runPackageCommand: async (args) => {
|
|
76
76
|
commands.push(args);
|
|
77
|
+
|
|
77
78
|
if (args[0] === "add") {
|
|
78
79
|
const packageRoot = join(root, "node_modules", "example");
|
|
79
80
|
|
|
@@ -112,6 +113,7 @@ describe("cueloop install", () => {
|
|
|
112
113
|
installRoot: root,
|
|
113
114
|
runPackageCommand: async (args) => {
|
|
114
115
|
commands.push(args);
|
|
116
|
+
|
|
115
117
|
if (args.at(-1) === "example@2.0.0") {
|
|
116
118
|
writeFileSync(
|
|
117
119
|
join(root, "package.json"),
|
package/src/install-command.ts
CHANGED
|
@@ -34,6 +34,7 @@ function previousPackageSpec(installRoot: string, name: string): string | null {
|
|
|
34
34
|
const path = join(installRoot, "node_modules", name, "package.json");
|
|
35
35
|
|
|
36
36
|
if (!existsSync(path)) return null;
|
|
37
|
+
|
|
37
38
|
try {
|
|
38
39
|
const parsed = v.safeParse(
|
|
39
40
|
v.object({ version: v.pipe(v.string(), v.minLength(1)) }),
|
|
@@ -59,6 +60,7 @@ export async function installExtensionCommand(
|
|
|
59
60
|
|
|
60
61
|
return 2;
|
|
61
62
|
}
|
|
63
|
+
|
|
62
64
|
const name = match[1]!;
|
|
63
65
|
const spec = source!.slice("npm:".length);
|
|
64
66
|
const installRoot = options.installRoot ?? extensionInstallRoot();
|
|
@@ -74,6 +76,7 @@ export async function installExtensionCommand(
|
|
|
74
76
|
JSON.stringify({ name: "cueloop-extensions", private: true }, null, 2),
|
|
75
77
|
);
|
|
76
78
|
}
|
|
79
|
+
|
|
77
80
|
const { exitCode, stderr } = await runCommand([
|
|
78
81
|
"add",
|
|
79
82
|
"--ignore-scripts",
|
|
@@ -87,6 +90,7 @@ export async function installExtensionCommand(
|
|
|
87
90
|
|
|
88
91
|
return 1;
|
|
89
92
|
}
|
|
93
|
+
|
|
90
94
|
const discovered = discoverInstalledExtensionPackages(installRoot);
|
|
91
95
|
const installed = discovered.packages.find((entry) => entry.name === name);
|
|
92
96
|
|
|
@@ -107,6 +111,7 @@ export async function installExtensionCommand(
|
|
|
107
111
|
|
|
108
112
|
return 1;
|
|
109
113
|
}
|
|
114
|
+
|
|
110
115
|
console.log(
|
|
111
116
|
`Installed ${name}. Restart cueloop and run 'cueloop restart' to load daemon capabilities.`,
|
|
112
117
|
);
|
package/src/main.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* observers), `cueloop daemon` runs the daemon in the foreground.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { parseHarnessSelection, assertDaemonHarness } from "./harness-selection";
|
|
11
12
|
import { basename, resolve } from "node:path";
|
|
12
13
|
import { parseArgs, stringFlag, type ParsedArgs } from "./args";
|
|
13
14
|
import {
|
|
@@ -29,7 +30,11 @@ import { openReview } from "@cueloop/daemon/thread-review";
|
|
|
29
30
|
// The CLI owns adapter composition; the daemon package still starts independently.
|
|
30
31
|
process.env.CUELOOP_DAEMON_ENTRY = import.meta.path;
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const harnessSelection = parseHarnessSelection(process.argv.slice(2));
|
|
34
|
+
const argv = harnessSelection.args;
|
|
35
|
+
|
|
36
|
+
if (harnessSelection.harness) process.env.CUELOOP_AGENT_HARNESS = harnessSelection.harness;
|
|
37
|
+
|
|
33
38
|
const cmd = argv[0];
|
|
34
39
|
|
|
35
40
|
async function serveEntry(rest: string[]): Promise<number> {
|
|
@@ -116,20 +121,25 @@ const helpAliases = new Set(["-h", "--help", "help"]);
|
|
|
116
121
|
|
|
117
122
|
async function main(): Promise<number> {
|
|
118
123
|
if (cmd === undefined) return runTui();
|
|
124
|
+
|
|
119
125
|
const handler = commandHandlers[cmd];
|
|
120
126
|
|
|
121
127
|
if (handler !== undefined) return handler(argv.slice(1));
|
|
128
|
+
|
|
122
129
|
if (versionAliases.has(cmd)) {
|
|
123
130
|
console.log(CLI_VERSION);
|
|
124
131
|
|
|
125
132
|
return 0;
|
|
126
133
|
}
|
|
134
|
+
|
|
127
135
|
if (helpAliases.has(cmd)) {
|
|
128
136
|
printHelp();
|
|
129
137
|
|
|
130
138
|
return 0;
|
|
131
139
|
}
|
|
140
|
+
|
|
132
141
|
if (cmd.startsWith("ses_")) return runTui(cmd);
|
|
142
|
+
|
|
133
143
|
printHelp();
|
|
134
144
|
|
|
135
145
|
return 2;
|
|
@@ -170,11 +180,13 @@ async function openReviewOfKind(
|
|
|
170
180
|
const target = resolveOpenTarget(sessions, { match, selector });
|
|
171
181
|
|
|
172
182
|
if (target.kind === "session") return runTui(target.sessionId, layout);
|
|
183
|
+
|
|
173
184
|
if (target.kind === "no-pending" && emptyMessage !== undefined) {
|
|
174
185
|
console.error(emptyMessage);
|
|
175
186
|
|
|
176
187
|
return 1;
|
|
177
188
|
}
|
|
189
|
+
|
|
178
190
|
console.error(openTargetMessage(label, target));
|
|
179
191
|
|
|
180
192
|
return 1;
|
|
@@ -223,6 +235,7 @@ async function prototypeCommand(argv: string[]): Promise<number> {
|
|
|
223
235
|
|
|
224
236
|
return 1;
|
|
225
237
|
}
|
|
238
|
+
|
|
226
239
|
const client = await DaemonClient.connect({ autostart: true });
|
|
227
240
|
const review = await openReview(client, {
|
|
228
241
|
type: "prototype",
|
|
@@ -262,6 +275,7 @@ async function diffCommand(argv: string[]): Promise<number> {
|
|
|
262
275
|
initialReview = await client.workbenchReview(cwd);
|
|
263
276
|
} catch (error) {
|
|
264
277
|
if (!(error instanceof DaemonClientError) || error.code !== "unknown_method") throw error;
|
|
278
|
+
|
|
265
279
|
const session = await client.sessionWorkbench(cwd);
|
|
266
280
|
const diff = await client.repoDiff(cwd);
|
|
267
281
|
|
|
@@ -286,12 +300,14 @@ async function pairCommand(argv: string[]): Promise<number> {
|
|
|
286
300
|
|
|
287
301
|
if (sessionId !== undefined) {
|
|
288
302
|
thread = await client.sessionGet(sessionId);
|
|
303
|
+
|
|
289
304
|
if (thread.artifact.meta.workbench !== true || thread.status !== "pending") {
|
|
290
305
|
throw new Error(`Thread ${sessionId} is not an open workbench`);
|
|
291
306
|
}
|
|
292
307
|
} else {
|
|
293
308
|
thread = await client.sessionWorkbench(process.cwd());
|
|
294
309
|
}
|
|
310
|
+
|
|
295
311
|
if (parsed.flags["open-tab"] === true) {
|
|
296
312
|
const { openHerdrThreadSurface } = await import("@cueloop/daemon/herdr-thread-surface");
|
|
297
313
|
|
|
@@ -300,6 +316,7 @@ async function pairCommand(argv: string[]): Promise<number> {
|
|
|
300
316
|
} finally {
|
|
301
317
|
client.close();
|
|
302
318
|
}
|
|
319
|
+
|
|
303
320
|
if (parsed.flags["no-tui"] === true || parsed.flags["open-tab"] === true) {
|
|
304
321
|
console.log(
|
|
305
322
|
JSON.stringify({ threadId: thread.id, startedAt, ...(openStatus && { openStatus }) }),
|
|
@@ -405,13 +422,27 @@ function printHelp(): void {
|
|
|
405
422
|
" cueloop review-comment <id> add or update an agent PR finding (see --help)",
|
|
406
423
|
" cueloop daemon run the daemon in the foreground",
|
|
407
424
|
" cueloop stop stop the local daemon",
|
|
408
|
-
" cueloop restart
|
|
425
|
+
" cueloop restart [--harness pi|fx] restart with the preferred Thread harness",
|
|
409
426
|
" cueloop dev open the TUI on an isolated home seeded with example threads",
|
|
410
427
|
"",
|
|
428
|
+
" cueloop --harness pi|fx select the default for new Threads",
|
|
411
429
|
" cueloop -v, --version print the installed version",
|
|
412
430
|
" cueloop -h, --help print this help",
|
|
413
431
|
].join("\n"),
|
|
414
432
|
);
|
|
415
433
|
}
|
|
416
434
|
|
|
435
|
+
if (cmd === "dev") (await import("./dev-command")).configureDevHome();
|
|
436
|
+
|
|
437
|
+
if (
|
|
438
|
+
harnessSelection.harness &&
|
|
439
|
+
cmd !== "restart" &&
|
|
440
|
+
cmd !== "daemon" &&
|
|
441
|
+
cmd !== "--help" &&
|
|
442
|
+
cmd !== "-h" &&
|
|
443
|
+
cmd !== "--version" &&
|
|
444
|
+
cmd !== "-v"
|
|
445
|
+
)
|
|
446
|
+
await assertDaemonHarness(harnessSelection.harness);
|
|
447
|
+
|
|
417
448
|
process.exit(await main());
|
package/src/open-target.ts
CHANGED
|
@@ -94,6 +94,7 @@ export function resolveOpenTarget(sessions: Thread[], query: OpenTargetQuery): O
|
|
|
94
94
|
.sort(newestFirst);
|
|
95
95
|
|
|
96
96
|
if (substringMatches.length === 1) return { kind: "session", sessionId: substringMatches[0]!.id };
|
|
97
|
+
|
|
97
98
|
if (substringMatches.length === 0) return { kind: "no-match", selector };
|
|
98
99
|
|
|
99
100
|
return {
|
package/src/pr.ts
CHANGED
|
@@ -65,7 +65,9 @@ function isReviewSeverity(value: string): value is ReviewSeverity {
|
|
|
65
65
|
|
|
66
66
|
function reviewEvent(value: string): GitHubReviewEvent | undefined {
|
|
67
67
|
if (value === "comment") return "COMMENT";
|
|
68
|
+
|
|
68
69
|
if (value === "approve") return "APPROVE";
|
|
70
|
+
|
|
69
71
|
if (value === "request-changes") return "REQUEST_CHANGES";
|
|
70
72
|
|
|
71
73
|
return undefined;
|
|
@@ -75,6 +77,7 @@ function reviewSummary(event: GitHubReviewEvent, body?: string): string | undefi
|
|
|
75
77
|
const trimmed = body?.trim();
|
|
76
78
|
|
|
77
79
|
if (trimmed) return trimmed;
|
|
80
|
+
|
|
78
81
|
if (event === "APPROVE") return undefined;
|
|
79
82
|
|
|
80
83
|
return "Review submitted with cueloop.";
|
|
@@ -115,6 +118,7 @@ async function reanchorReviewComments(
|
|
|
115
118
|
`finding ${annotation.id} is outdated or ambiguous - refresh the finding before posting`,
|
|
116
119
|
);
|
|
117
120
|
}
|
|
121
|
+
|
|
118
122
|
const line = finding.side === "RIGHT" ? matched.newLine! : matched.oldLine!;
|
|
119
123
|
let startLine: number | undefined;
|
|
120
124
|
|
|
@@ -124,6 +128,7 @@ async function reanchorReviewComments(
|
|
|
124
128
|
`finding ${annotation.id} has no start anchor - refresh the finding before posting`,
|
|
125
129
|
);
|
|
126
130
|
}
|
|
131
|
+
|
|
127
132
|
const matchedStart = resolveLine(finding.startAnchor);
|
|
128
133
|
|
|
129
134
|
if (!matchedStart) {
|
|
@@ -131,6 +136,7 @@ async function reanchorReviewComments(
|
|
|
131
136
|
`finding ${annotation.id} start is outdated or ambiguous - refresh the finding before posting`,
|
|
132
137
|
);
|
|
133
138
|
}
|
|
139
|
+
|
|
134
140
|
startLine = finding.side === "RIGHT" ? matchedStart.newLine! : matchedStart.oldLine!;
|
|
135
141
|
|
|
136
142
|
if (startLine >= line) {
|
|
@@ -174,11 +180,16 @@ function reviewFindingInput(flags: Record<string, string | boolean>): ReviewFind
|
|
|
174
180
|
const suggestion = textFlag(flags, "suggestion", "suggestion-file");
|
|
175
181
|
|
|
176
182
|
if (!body) throw new Error("--body or --body-file is required");
|
|
183
|
+
|
|
177
184
|
if (side !== "LEFT" && side !== "RIGHT") throw new Error("--side must be LEFT or RIGHT");
|
|
185
|
+
|
|
178
186
|
if (!isReviewSeverity(severity)) throw new Error("--severity must be p0, p1, or p2");
|
|
187
|
+
|
|
179
188
|
if (!Number.isInteger(line) || line < 1) throw new Error("--line must be a positive integer");
|
|
189
|
+
|
|
180
190
|
if (startLine !== undefined && (!Number.isInteger(startLine) || startLine < 1))
|
|
181
191
|
throw new Error("--start-line must be a positive integer");
|
|
192
|
+
|
|
182
193
|
if (suggestion?.trim() && side !== "RIGHT")
|
|
183
194
|
throw new Error("suggestions must target the RIGHT side of a diff");
|
|
184
195
|
|
|
@@ -216,12 +227,14 @@ async function reviewFindingAnnotation(
|
|
|
216
227
|
throw new Error(
|
|
217
228
|
`${input.path}:${input.line} is not present on the ${input.side} side of this diff`,
|
|
218
229
|
);
|
|
230
|
+
|
|
219
231
|
const fileRange = fileRowRange(rows, input.path)!;
|
|
220
232
|
const fileRows = rows.slice(fileRange.start, fileRange.end);
|
|
221
233
|
let startAnchor: Annotation["anchor"] | undefined;
|
|
222
234
|
|
|
223
235
|
if (input.startLine !== undefined) {
|
|
224
236
|
if (input.startLine >= input.line) throw new Error("--start-line must be before --line");
|
|
237
|
+
|
|
225
238
|
const startRowIndex = rows.findIndex(
|
|
226
239
|
(candidate) =>
|
|
227
240
|
candidate.file === input.path &&
|
|
@@ -234,8 +247,10 @@ async function reviewFindingAnnotation(
|
|
|
234
247
|
throw new Error(
|
|
235
248
|
`${input.path}:${input.startLine} is not present on the ${input.side} side of this diff`,
|
|
236
249
|
);
|
|
250
|
+
|
|
237
251
|
startAnchor = diffRowAnchor(fileRows, startRowIndex - fileRange.start);
|
|
238
252
|
}
|
|
253
|
+
|
|
239
254
|
const id = input.id ?? nextFindingId(session.annotations);
|
|
240
255
|
const existing = session.annotations.find((annotation) => annotation.id === id);
|
|
241
256
|
|
|
@@ -275,6 +290,7 @@ export async function reviewCommentCommand(argv: string[]): Promise<number> {
|
|
|
275
290
|
|
|
276
291
|
return 2;
|
|
277
292
|
}
|
|
293
|
+
|
|
278
294
|
try {
|
|
279
295
|
const client = await DaemonClient.connect({ autostart: true, role: "agent", author: "agent" });
|
|
280
296
|
|
|
@@ -317,6 +333,7 @@ export async function reviewCommand(argv: string[]): Promise<number> {
|
|
|
317
333
|
|
|
318
334
|
return 2;
|
|
319
335
|
}
|
|
336
|
+
|
|
320
337
|
let imported: Awaited<ReturnType<ReturnType<typeof forge>["importPullRequest"]>>;
|
|
321
338
|
|
|
322
339
|
try {
|
|
@@ -335,6 +352,7 @@ export async function reviewCommand(argv: string[]): Promise<number> {
|
|
|
335
352
|
|
|
336
353
|
return 1;
|
|
337
354
|
}
|
|
355
|
+
|
|
338
356
|
const client = await DaemonClient.connect({ autostart: true });
|
|
339
357
|
// A PR diff is a partial patch with no full file contents, so no `files` here;
|
|
340
358
|
// hunk curation stays disabled for PR reviews (see diff-hunk-curate.ts).
|
|
@@ -384,6 +402,7 @@ export async function reviewOpenCommand(argv: string[]): Promise<number> {
|
|
|
384
402
|
|
|
385
403
|
return 2;
|
|
386
404
|
}
|
|
405
|
+
|
|
387
406
|
const client = await DaemonClient.connect({ autostart: true });
|
|
388
407
|
|
|
389
408
|
try {
|
|
@@ -417,6 +436,7 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
|
|
|
417
436
|
|
|
418
437
|
return 2;
|
|
419
438
|
}
|
|
439
|
+
|
|
420
440
|
const session = await getSession(sessionId);
|
|
421
441
|
const pr = session.artifact.meta.pr;
|
|
422
442
|
|
|
@@ -430,11 +450,13 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
|
|
|
430
450
|
|
|
431
451
|
return 1;
|
|
432
452
|
}
|
|
453
|
+
|
|
433
454
|
if (session.status !== "resolved" || !session.message) {
|
|
434
455
|
console.error(`session ${sessionId} is unresolved - approve or request changes before posting`);
|
|
435
456
|
|
|
436
457
|
return 1;
|
|
437
458
|
}
|
|
459
|
+
|
|
438
460
|
const selectedIds = new Set(
|
|
439
461
|
(stringFlag(flags, "comments") ?? "")
|
|
440
462
|
.split(",")
|
|
@@ -453,6 +475,7 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
|
|
|
453
475
|
|
|
454
476
|
return 1;
|
|
455
477
|
}
|
|
478
|
+
|
|
456
479
|
const eventFlag = stringFlag(flags, "event") ?? "comment";
|
|
457
480
|
const event = reviewEvent(eventFlag);
|
|
458
481
|
|
|
@@ -461,6 +484,7 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
|
|
|
461
484
|
|
|
462
485
|
return 2;
|
|
463
486
|
}
|
|
487
|
+
|
|
464
488
|
try {
|
|
465
489
|
const body = reviewSummary(event, textFlag(flags, "body", "body-file"));
|
|
466
490
|
const forgePort = forge();
|
|
@@ -475,10 +499,12 @@ export async function reviewPostCommand(argv: string[]): Promise<number> {
|
|
|
475
499
|
) {
|
|
476
500
|
throw new Error("pull request changed since this review - refresh before posting");
|
|
477
501
|
}
|
|
502
|
+
|
|
478
503
|
const comments = await reanchorReviewComments(session, selectedComments);
|
|
479
504
|
const number = Number(new URL(session.artifact.meta.prUrl).pathname.split("/").at(-1));
|
|
480
505
|
|
|
481
506
|
if (!Number.isInteger(number)) throw new Error("pull request URL has no numeric PR number");
|
|
507
|
+
|
|
482
508
|
const posted = await forgePort.postPullRequestComments({
|
|
483
509
|
publicationId: reviewPublicationId(
|
|
484
510
|
session,
|
package/src/share-command.ts
CHANGED
|
@@ -80,6 +80,7 @@ export async function shareSession(
|
|
|
80
80
|
|
|
81
81
|
return 1;
|
|
82
82
|
}
|
|
83
|
+
|
|
83
84
|
const session = params.fork ? await client.sessionFork(picked.id) : picked;
|
|
84
85
|
// a workbench thread renders live locally; freeze its diff so the remote reviewer gets a stable snapshot
|
|
85
86
|
const shared = client.repoDiff
|
|
@@ -98,7 +99,9 @@ export async function shareSession(
|
|
|
98
99
|
shareBranch: session.history?.branch ?? "main",
|
|
99
100
|
},
|
|
100
101
|
]);
|
|
102
|
+
|
|
101
103
|
if (params.fork) deps.out(`forked ${picked.id} as ${session.id}`);
|
|
104
|
+
|
|
102
105
|
deps.out(copied ? `share link copied - ${line}` : line);
|
|
103
106
|
|
|
104
107
|
return 0;
|
|
@@ -121,6 +124,7 @@ export async function pullSession(
|
|
|
121
124
|
|
|
122
125
|
return 1;
|
|
123
126
|
}
|
|
127
|
+
|
|
124
128
|
const remote = await deps.pull(session.shares!.at(-1)!.id, {
|
|
125
129
|
host: params.host,
|
|
126
130
|
port: params.port,
|
|
@@ -143,6 +147,7 @@ export async function pullSession(
|
|
|
143
147
|
/** The named session, or the most recent one when no id is given. */
|
|
144
148
|
async function pickSession(client: ThreadClient, sessionId?: string): Promise<Thread | null> {
|
|
145
149
|
if (sessionId) return client.sessionGet(sessionId);
|
|
150
|
+
|
|
146
151
|
const sessions = await client.sessionList();
|
|
147
152
|
|
|
148
153
|
return sessions.at(-1) ?? null;
|
|
@@ -155,6 +160,7 @@ async function pickSharedSession(client: ThreadClient, sessionId?: string): Prom
|
|
|
155
160
|
|
|
156
161
|
return session.shares?.length ? session : null;
|
|
157
162
|
}
|
|
163
|
+
|
|
158
164
|
const sessions = await client.sessionList();
|
|
159
165
|
|
|
160
166
|
return sessions.filter((session) => session.shares?.length).at(-1) ?? null;
|
package/src/thread-commands.ts
CHANGED
|
@@ -46,6 +46,7 @@ async function sessionCreate({ client, flags }: SessionContext): Promise<number>
|
|
|
46
46
|
|
|
47
47
|
return 2;
|
|
48
48
|
}
|
|
49
|
+
|
|
49
50
|
const contentFile = stringFlag(flags, "content-file");
|
|
50
51
|
const content = contentFile ? await Bun.file(contentFile).text() : await readStdin();
|
|
51
52
|
// per-file agent notes for diff sessions: a JSON array of { path, body }
|
|
@@ -76,6 +77,7 @@ async function sessionCreate({ client, flags }: SessionContext): Promise<number>
|
|
|
76
77
|
if (openResult !== "opened" && openResult !== "focused") {
|
|
77
78
|
console.error(manualThreadOpenCommand(review.id));
|
|
78
79
|
}
|
|
80
|
+
|
|
79
81
|
out(review.session);
|
|
80
82
|
|
|
81
83
|
return 0;
|
|
@@ -113,6 +115,7 @@ async function sessionWaitCommand({ client, positional, flags }: SessionContext)
|
|
|
113
115
|
|
|
114
116
|
return 0;
|
|
115
117
|
}
|
|
118
|
+
|
|
116
119
|
const { allow, message } = messageResponse(session);
|
|
117
120
|
|
|
118
121
|
out({ status: "resolved", allow, message });
|
|
@@ -143,6 +146,7 @@ async function annotateAnchor(
|
|
|
143
146
|
? { anchor: root.anchor, replyTo: root.replyTo ?? root.id }
|
|
144
147
|
: { anchor: root.anchor, replyTo: root.replyTo ?? root.id, target: root.target };
|
|
145
148
|
}
|
|
149
|
+
|
|
146
150
|
const selector = stringFlag(flags, "selector");
|
|
147
151
|
const quote =
|
|
148
152
|
selector === undefined
|
|
@@ -177,6 +181,7 @@ async function sessionAnnotateCommand({
|
|
|
177
181
|
};
|
|
178
182
|
|
|
179
183
|
if (replyTo !== undefined) base.replyTo = replyTo;
|
|
184
|
+
|
|
180
185
|
const annotation: Omit<Annotation, "createdAt"> =
|
|
181
186
|
target === undefined ? base : { ...base, target };
|
|
182
187
|
|
|
@@ -351,7 +356,9 @@ async function sessionEventsCommand({
|
|
|
351
356
|
const stream = new Promise<number>((resolve) => {
|
|
352
357
|
client.onEvent((event) => {
|
|
353
358
|
if (event.sessionId !== id) return;
|
|
359
|
+
|
|
354
360
|
out(event);
|
|
361
|
+
|
|
355
362
|
if (once) resolve(0);
|
|
356
363
|
});
|
|
357
364
|
});
|
|
@@ -359,6 +366,7 @@ async function sessionEventsCommand({
|
|
|
359
366
|
const disconnect = new Promise<number>((resolve) => {
|
|
360
367
|
client.onDisconnect(() => {
|
|
361
368
|
if (finished) return;
|
|
369
|
+
|
|
362
370
|
console.error("daemon connection closed - restart cueloop session events to resume");
|
|
363
371
|
resolve(1);
|
|
364
372
|
});
|
|
@@ -366,6 +374,7 @@ async function sessionEventsCommand({
|
|
|
366
374
|
|
|
367
375
|
await client.subscribe();
|
|
368
376
|
await client.sessionGet(id);
|
|
377
|
+
|
|
369
378
|
if (ready) console.log(JSON.stringify({ event: "events.ready", sessionId: id }));
|
|
370
379
|
|
|
371
380
|
const code = await Promise.race([stream, disconnect]);
|
|
@@ -546,9 +555,11 @@ async function annotateBody(
|
|
|
546
555
|
const explicit = stringFlag(flags, "body");
|
|
547
556
|
|
|
548
557
|
if (explicit !== undefined) return explicit;
|
|
558
|
+
|
|
549
559
|
const actionRef = stringFlag(flags, "action");
|
|
550
560
|
|
|
551
561
|
if (actionRef === undefined) return "";
|
|
562
|
+
|
|
552
563
|
const session = await client.sessionGet(id);
|
|
553
564
|
const actions = loadConfig({ repoRoot: session.workspace.repoRoot }).actions;
|
|
554
565
|
const action = resolveQuickAction(actions, actionRef);
|
package/src/update-command.ts
CHANGED
|
@@ -52,6 +52,7 @@ async function fetchLatestVersion(): Promise<string | undefined> {
|
|
|
52
52
|
const response = await fetch(RELEASES_URL, { headers: { "user-agent": "cueloop-update" } });
|
|
53
53
|
|
|
54
54
|
if (!response.ok) return undefined;
|
|
55
|
+
|
|
55
56
|
const parsed = v.safeParse(ReleasesSchema, await response.json());
|
|
56
57
|
|
|
57
58
|
if (!parsed.success) return undefined;
|
|
@@ -87,6 +88,7 @@ async function runInstaller(targetInstallDir: string): Promise<number> {
|
|
|
87
88
|
|
|
88
89
|
return 1;
|
|
89
90
|
}
|
|
91
|
+
|
|
90
92
|
const parsed = v.safeParse(InstallerScriptSchema, await response.text());
|
|
91
93
|
|
|
92
94
|
if (!parsed.success) {
|
|
@@ -94,6 +96,7 @@ async function runInstaller(targetInstallDir: string): Promise<number> {
|
|
|
94
96
|
|
|
95
97
|
return 1;
|
|
96
98
|
}
|
|
99
|
+
|
|
97
100
|
const child = Bun.spawn(["sh"], {
|
|
98
101
|
stdin: "pipe",
|
|
99
102
|
stdout: "inherit",
|
|
@@ -143,11 +146,13 @@ export async function runUpdate(deps: UpdateDeps, dryRun: boolean): Promise<numb
|
|
|
143
146
|
|
|
144
147
|
return 1;
|
|
145
148
|
}
|
|
149
|
+
|
|
146
150
|
if (!targetInstallDir.startsWith("/")) {
|
|
147
151
|
deps.error("cueloop update: CUELOOP_INSTALL_DIR must be an absolute path");
|
|
148
152
|
|
|
149
153
|
return 1;
|
|
150
154
|
}
|
|
155
|
+
|
|
151
156
|
deps.out(`Current version: ${deps.currentVersion}`);
|
|
152
157
|
|
|
153
158
|
if (dryRun) {
|
|
@@ -155,6 +160,7 @@ export async function runUpdate(deps: UpdateDeps, dryRun: boolean): Promise<numb
|
|
|
155
160
|
|
|
156
161
|
return 0;
|
|
157
162
|
}
|
|
163
|
+
|
|
158
164
|
deps.out("Checking for updates to latest version...");
|
|
159
165
|
const latest = await deps.fetchLatestVersion();
|
|
160
166
|
|
|
@@ -163,6 +169,7 @@ export async function runUpdate(deps: UpdateDeps, dryRun: boolean): Promise<numb
|
|
|
163
169
|
|
|
164
170
|
return 0;
|
|
165
171
|
}
|
|
172
|
+
|
|
166
173
|
deps.out(`updating cueloop in ${targetInstallDir}...`);
|
|
167
174
|
const exitCode = await deps.runInstaller(targetInstallDir);
|
|
168
175
|
|