cueloop 0.1.0-alpha.96 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cueloop",
3
- "version": "0.1.0-alpha.96",
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.96",
34
- "@cueloop/client": "0.1.0-alpha.96",
35
- "@cueloop/daemon": "0.1.0-alpha.96",
36
- "@cueloop/extension-api": "0.1.0-alpha.96",
37
- "@cueloop/schema": "0.1.0-alpha.96",
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
- while (!existsSync(join(home, "delivered.txt")) && Date.now() < deadline) await Bun.sleep(20);
65
- expect(readFileSync(join(home, "delivered.txt"), "utf8")).toContain("Continue after MCP exit.");
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
+ }
@@ -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 ??= join(homedir(), ".cueloop-dev"));
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),