cueloop 0.1.0-alpha.97 → 0.1.0-alpha.98
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/agent-harness.ts +17 -0
- package/src/codex-delivery-worker-command.test.ts +8 -2
- package/src/daemon-command.ts +38 -0
- package/src/dev-command.ts +10 -2
- package/src/main.ts +4 -23
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.98",
|
|
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 --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.98",
|
|
34
|
+
"@cueloop/client": "0.1.0-alpha.98",
|
|
35
|
+
"@cueloop/daemon": "0.1.0-alpha.98",
|
|
36
|
+
"@cueloop/extension-api": "0.1.0-alpha.98",
|
|
37
|
+
"@cueloop/schema": "0.1.0-alpha.98",
|
|
38
38
|
"@modelcontextprotocol/server": "2.0.0",
|
|
39
39
|
"@valibot/to-json-schema": "1.8.0",
|
|
40
40
|
"valibot": "^1.5.0"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createFxHarness } from "@cueloop/adapters/fx/harness";
|
|
2
|
+
import { loadConfig } from "@cueloop/client/config";
|
|
3
|
+
import type { AgentHarnessAdapter } from "@cueloop/schema";
|
|
4
|
+
|
|
5
|
+
/** Select a locally installed harness; unsupported providers fail before a daemon starts. */
|
|
6
|
+
export function configuredAgentHarness(): AgentHarnessAdapter | undefined {
|
|
7
|
+
const id = process.env.CUELOOP_AGENT_HARNESS ?? "fx";
|
|
8
|
+
|
|
9
|
+
if (id !== "fx") throw new Error(`Thread agent harness is not installed: ${id}`);
|
|
10
|
+
|
|
11
|
+
return createFxHarness({ command: [process.env.CUELOOP_FX_EXECUTABLE ?? "fx", "acp"] });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Resolve the experimental Thread agent flag from user and workspace TOML. */
|
|
15
|
+
export function threadAgentEnabled(repoRoot: string): boolean {
|
|
16
|
+
return loadConfig({ repoRoot }).experimental.threadAgent;
|
|
17
|
+
}
|
|
@@ -61,8 +61,14 @@ test("a Codex reply is delivered without an MCP transport", async () => {
|
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
63
|
await client.sessionSendMessage(opened.threadId, "approved", "Continue after MCP exit.");
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const deliveryPath = join(home, "delivered.txt");
|
|
65
|
+
const delivered = () =>
|
|
66
|
+
existsSync(deliveryPath) &&
|
|
67
|
+
readFileSync(deliveryPath, "utf8").includes("Continue after MCP exit.");
|
|
68
|
+
|
|
69
|
+
// Shell redirection creates the file before printf writes the delivered message.
|
|
70
|
+
while (!delivered() && Date.now() < deadline) await Bun.sleep(20);
|
|
71
|
+
expect(delivered()).toBe(true);
|
|
66
72
|
} finally {
|
|
67
73
|
client.close();
|
|
68
74
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Compose the local daemon with its installed harness adapters at the CLI boundary. */
|
|
2
|
+
export async function daemonCommand(argv: string[]): Promise<number> {
|
|
3
|
+
const { DaemonServer } = await import("@cueloop/daemon");
|
|
4
|
+
// Explicit foreground never idle-exits; --autostart (a re-exec from the client)
|
|
5
|
+
// takes the normal idle-exit, matching the source main.ts entry.
|
|
6
|
+
const envIdle = process.env.CUELOOP_IDLE_EXIT_MS;
|
|
7
|
+
let idleExitMs: number | undefined = 0;
|
|
8
|
+
|
|
9
|
+
if (argv.includes("--autostart")) idleExitMs = envIdle ? Number(envIdle) : undefined;
|
|
10
|
+
const { configuredAgentHarness, threadAgentEnabled } = await import("./agent-harness");
|
|
11
|
+
const server = new DaemonServer({
|
|
12
|
+
idleExitMs,
|
|
13
|
+
threadAgent: {
|
|
14
|
+
enabled: true,
|
|
15
|
+
enabledForThread: (thread) => threadAgentEnabled(thread.workspace.repoRoot),
|
|
16
|
+
adapter: configuredAgentHarness(),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
const path = server.start();
|
|
20
|
+
|
|
21
|
+
if (path === null) {
|
|
22
|
+
console.error("a cueloop daemon already owns this home - nothing to do");
|
|
23
|
+
|
|
24
|
+
return 1;
|
|
25
|
+
}
|
|
26
|
+
console.log(`cueloop daemon (foreground) on ${path}`);
|
|
27
|
+
const stop = () => {
|
|
28
|
+
server.stop();
|
|
29
|
+
process.exit(0);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
process.once("SIGINT", stop);
|
|
33
|
+
process.once("SIGTERM", stop);
|
|
34
|
+
process.once("exit", () => server.stop());
|
|
35
|
+
await new Promise(() => {}); // run until signalled
|
|
36
|
+
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
package/src/dev-command.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { threadAgentEnabled } from "./agent-harness";
|
|
1
2
|
/**
|
|
2
3
|
* `cueloop dev`: open the TUI against an isolated dev home seeded once with a
|
|
3
4
|
* plan and a diff under this repo's project plus a standalone reply thread, so
|
|
@@ -97,16 +98,23 @@ async function refreshDevSeed(client: DaemonClient, home: string): Promise<void>
|
|
|
97
98
|
|
|
98
99
|
/** `cueloop dev`: seed an isolated dev home (refreshing a stale seed), then open the TUI on it. */
|
|
99
100
|
export async function devCommand(): Promise<number> {
|
|
100
|
-
const home = (process.env.CUELOOP_HOME ??=
|
|
101
|
+
const home = (process.env.CUELOOP_HOME ??= threadAgentEnabled(process.cwd())
|
|
102
|
+
? join(process.cwd(), ".cueloop-dev")
|
|
103
|
+
: join(homedir(), ".cueloop-dev"));
|
|
101
104
|
|
|
105
|
+
let sessionId: string | undefined;
|
|
102
106
|
const client = await DaemonClient.connect({ autostart: true });
|
|
103
107
|
try {
|
|
104
108
|
await refreshDevSeed(client, home);
|
|
109
|
+
if (threadAgentEnabled(process.cwd()))
|
|
110
|
+
sessionId = (await client.sessionList()).find(
|
|
111
|
+
(thread) => thread.artifact.meta.title === "Read the repository",
|
|
112
|
+
)?.id;
|
|
105
113
|
} finally {
|
|
106
114
|
client.close();
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
const { runClient } = await import("@cueloop/client");
|
|
110
118
|
|
|
111
|
-
return runClient({});
|
|
119
|
+
return runClient({ sessionId });
|
|
112
120
|
}
|
package/src/main.ts
CHANGED
|
@@ -26,31 +26,12 @@ import { DaemonClient, DaemonClientError } from "@cueloop/daemon/client";
|
|
|
26
26
|
import type { DiffFileContents, Thread, ThreadSurfaceOpenStatus } from "@cueloop/schema";
|
|
27
27
|
import { openReview } from "@cueloop/daemon/thread-review";
|
|
28
28
|
|
|
29
|
+
// The CLI owns adapter composition; the daemon package still starts independently.
|
|
30
|
+
process.env.CUELOOP_DAEMON_ENTRY = import.meta.path;
|
|
31
|
+
|
|
29
32
|
const argv = process.argv.slice(2);
|
|
30
33
|
const cmd = argv[0];
|
|
31
34
|
|
|
32
|
-
async function daemonCommand(argv: string[]): Promise<number> {
|
|
33
|
-
const { DaemonServer } = await import("@cueloop/daemon");
|
|
34
|
-
// Explicit foreground never idle-exits; --autostart (a re-exec from the client)
|
|
35
|
-
// takes the normal idle-exit, matching the source main.ts entry.
|
|
36
|
-
const envIdle = process.env.CUELOOP_IDLE_EXIT_MS;
|
|
37
|
-
let idleExitMs: number | undefined = 0;
|
|
38
|
-
|
|
39
|
-
if (argv.includes("--autostart")) idleExitMs = envIdle ? Number(envIdle) : undefined;
|
|
40
|
-
const server = new DaemonServer({ idleExitMs });
|
|
41
|
-
const path = server.start();
|
|
42
|
-
|
|
43
|
-
if (path === null) {
|
|
44
|
-
console.error("a cueloop daemon already owns this home - nothing to do");
|
|
45
|
-
|
|
46
|
-
return 1;
|
|
47
|
-
}
|
|
48
|
-
console.log(`cueloop daemon (foreground) on ${path}`);
|
|
49
|
-
await new Promise(() => {}); // run until signalled
|
|
50
|
-
|
|
51
|
-
return 0;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
35
|
async function serveEntry(rest: string[]): Promise<number> {
|
|
55
36
|
const { positional, flags } = parseArgs(rest);
|
|
56
37
|
const port = stringFlag(flags, "port");
|
|
@@ -103,7 +84,7 @@ interface CommandHandlers {
|
|
|
103
84
|
|
|
104
85
|
const commandHandlers: CommandHandlers = {
|
|
105
86
|
session: (rest) => sessionCommand(rest),
|
|
106
|
-
daemon: (rest) => daemonCommand(rest),
|
|
87
|
+
daemon: async (rest) => (await import("./daemon-command")).daemonCommand(rest),
|
|
107
88
|
stop: async () => (await import("./daemon-control")).stopCommand(),
|
|
108
89
|
restart: async () => (await import("./daemon-control")).restartCommand(),
|
|
109
90
|
plan: (rest) => planCommand(rest),
|